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>
52 lines
2.4 KiB
JavaScript
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}`));
|