The Directory (#405): a search nothing matches says so and shows no tiles; a show's page draws what the directory says of it, so with a long description More opens it and Less closes it, a failing feed says why, its latest items are listed, someone who is not the directory's admin is offered neither Refresh nor Remove, and Subscribe there says so and opens the feed. Search, Back, Subscribe from a tile and an admin's Refresh and Remove were already tested. The page (#406), on a fixture show of four linked episodes: a schedule's bar tapped lights that many of the newest; Download latest 3 queues three and they download; v opens the item's own page; Unsubscribe from the feed's dialog leaves no feed open. Shift J and K move between feeds, Right and Left open and close a folder from the keyboard, r takes the lists again. The player keeps its speed and volume across a reload, reports a stall of over half a second to the server's log, and marks an item played to its end read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
5.3 KiB
TypeScript
98 lines
5.3 KiB
TypeScript
// Serves the fixture feeds so the daemon under test has something real to scan.
|
|
import http from 'node:http';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const dir = import.meta.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', () => {
|
|
// As TypeSafe answers a key it does not know (#271).
|
|
if (req.headers.authorization === 'Bearer bad-key') {
|
|
res.writeHead(401, { 'content-type': 'application/json' }).end('{"detail":"invalid api key"}');
|
|
return;
|
|
}
|
|
// As ipx asks TypeSafe's Jev: the item, and a question a Signal.
|
|
const { state, questions } = JSON.parse(body) as { state: unknown, questions: Record<string, { criteria: unknown, instructions: string }> };
|
|
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: Record<string, { noul: number }> = {};
|
|
for (const [id, q] of Object.entries(questions)) {
|
|
// A key's check asks about no Signal's words.
|
|
const words = /described by these words: ([^.]*)\./.exec(q.instructions)?.[1].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: string, title: string) => `<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;
|
|
}
|
|
// A show to download from by hand (#404): one episode sent slowly enough for its progress to be
|
|
// seen, and one that always fails. Their files are theirs alone, as enclosure URLs must be.
|
|
if (req.url === '/live.xml') {
|
|
const item = (guid: string, title: string, file: string) =>
|
|
`<item><guid>${guid}</guid><title>${title}</title><enclosure url="http://127.0.0.1:${port}/${file}" type="audio/mpeg" length="40000"/></item>`;
|
|
res.writeHead(200, { 'content-type': 'application/xml' }).end(`<?xml version="1.0"?><rss version="2.0"><channel>
|
|
<title>Live Show</title>${item('live-1', 'Slow episode', 'live-slow.mp3')}${item('live-2', 'Broken episode', 'live-broken.mp3')}</channel></rss>`);
|
|
return;
|
|
}
|
|
// Four episodes, newest first, for Download latest and a schedule's window (#406).
|
|
if (req.url === '/latest.xml') {
|
|
const item = (n: number) => `<item><guid>latest-${n}</guid><title>Latest ${n}</title><link>http://127.0.0.1:${port}/latest-${n}.html</link><pubDate>${new Date(Date.UTC(2026, 0, 10 - n)).toUTCString()}</pubDate>`
|
|
+ `<enclosure url="http://127.0.0.1:${port}/latest-${n}.mp3" type="audio/mpeg" length="40000"/></item>`;
|
|
res.writeHead(200, { 'content-type': 'application/xml' }).end(`<?xml version="1.0"?><rss version="2.0"><channel>
|
|
<title>Latest Show</title>${[1, 2, 3, 4].map(item).join('')}</channel></rss>`);
|
|
return;
|
|
}
|
|
if (/^\/latest-\d\.mp3$/.test(req.url ?? '')) {
|
|
const body = fs.readFileSync(path.join(dir, 'ep1.mp3'));
|
|
res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': body.length }).end(body);
|
|
return;
|
|
}
|
|
if (req.url === '/live-broken.mp3') { res.writeHead(500).end('broken'); return; }
|
|
if (req.url === '/live-slow.mp3') {
|
|
const body = fs.readFileSync(path.join(dir, 'ep1.mp3'));
|
|
res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': body.length });
|
|
// Ten parts, a fifth of a second apart: two seconds of progress.
|
|
let part = 0;
|
|
const step = Math.ceil(body.length / 10);
|
|
const next = () => {
|
|
if (part * step >= body.length) { res.end(); return; }
|
|
res.write(body.subarray(part * step, ++part * step));
|
|
setTimeout(next, 200);
|
|
};
|
|
next();
|
|
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'
|
|
// A picture a browser can draw: the JPEGs are a few bytes that only say they are one.
|
|
: file.endsWith('.png') ? 'image/png'
|
|
: 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}`));
|