Files
ipx/tests/ui/fixtures/serve.js
rays 20f53183ac Signals: Jev's request in the shape TypeSafe takes, and its reason kept when it refuses (#257)
Every call was refused with 422: a noul question's `criteria` is an object of what true and false
mean, and it was sent a sentence. The error kept only the status, from error_for_status, so the log
and the trace said 422 and not why; TypeSafe's body said "Input should be a valid dictionary"
at questions.3.noul.criteria. The body is now in the error and on the jev span, with the status.

A request refused as malformed (400, 422) ends the pass, since every other item's would be refused
the same way: the 50 warnings a pass were the same failure 50 times. The state is text now, as
TypeSafe's docs show it, rather than an object that sent null for a feed with no title, and the
questions are keyed signal_<id>. Checked against TypeSafe with the corrected request: 0.96 for an
item about the keywords, 0.02 for one that was not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:48:06 +00:00

52 lines
2.4 KiB
JavaScript

// Serves the fixture feeds so the daemon under test has something real to scan.
const http = require('http');
const fs = require('fs');
const path = require('path');
const dir = __dirname;
const port = Number(process.env.FIXTURE_PORT || 8792);
http.createServer((req, res) => {
// Jev, for Signals (#254): sure of an item whose text has a question's keyword in it. Refuses
// a request in the shape TypeSafe refuses, as it does (#257).
if (req.method === 'POST' && req.url === '/jev') {
let body = '';
req.on('data', c => { body += c; });
req.on('end', () => {
const { state, questions } = JSON.parse(body);
if (Object.values(questions).some(q => typeof q.criteria !== 'object')) {
res.writeHead(422, { 'content-type': 'application/json' }).end('{"detail":"criteria should be an object"}');
return;
}
const title = String(state).toLowerCase();
const answers = {};
for (const [id, q] of Object.entries(questions)) {
const words = q.instructions.replace(/^.*: /, '').replace(/\?$/, '').split(', ');
answers[id] = { noul: words.some(w => title.includes(w.toLowerCase())) ? 0.9 : 0.1 };
}
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ answers }));
});
return;
}
// Dated today, since a Signal looks at the last 30 days only.
if (req.url === '/signal.xml') {
const now = new Date().toUTCString();
const item = (guid, title) => `<item><guid>${guid}</guid><title>${title}</title><pubDate>${now}</pubDate></item>`;
res.writeHead(200, { 'content-type': 'application/xml' }).end(`<?xml version="1.0"?><rss version="2.0"><channel>
<title>Signal Show</title>${item('sig-1', 'Monaco qualifying')}${item('sig-2', 'Baking bread')}</channel></rss>`);
return;
}
const name = decodeURIComponent(req.url.split('?')[0].replace(/^\//, '')) || 'index';
const file = path.join(dir, path.basename(name));
fs.readFile(file, (err, body) => {
if (err) { res.writeHead(404).end('no'); return; }
const type = file.endsWith('.mp3') ? 'audio/mpeg'
: file.endsWith('.opml') ? 'text/x-opml'
// Artwork as an image, or /api/art refuses it as not one.
: file.endsWith('.jpg') ? 'image/jpeg'
: file.endsWith('.html') ? 'text/html' : 'application/xml';
res.writeHead(200, { 'content-type': type, 'content-length': body.length });
res.end(body);
});
}).listen(port, '127.0.0.1', () => console.log(`fixtures on ${port}`));