Files
stack/packages/webui/tests/serve.test.mjs
T
jason.woltjeandClaude Opus 5.5 1bdb6f9b13 feat(webui): console tokens and shell restyle (#1542, row 53)
The console takes the design package's tokens.css and icons.svg as byte
copies, guarded by a test, and a new shell.css holds the components:
command bar with freshness line, section list (a strip that scrolls
inside itself below 760px), dense table, inspector, status mark, chips,
copy command, toast, Ctrl+K palette, not found, banners and skeleton
rows. JetBrains Mono 2.304 ships from the official archive with sha256
and OFL. Harbor following the system setting is the default palette.
A refusal (403, not-configured, no-bus-host, or the board's 403) fails
closed everywhere: no kept rows, counts, inspector, palette entries or
conversation history, and a later 500 or 503 doesn't bring them back.

Dewey built it in three rounds. Round 1 (75569953) and round 2
(78c3aef9) got changes from Darkwing (27148, 27165) and Filbert (27143,
27163) for refusal leaks outside the main view; round 3 (2562c05d) was
approved by Darkwing (27171) and Filbert (27174). Sage's gate on
ea080fd8 plus the candidate: runs 41/0, queue 148/0, webui 27/0,
conversation 182/0, control-board 124/0, build-tokens --check current,
every scripts/test-*.sh 0 failed (task 98/0 and release 14/0 with
Docker). The round 2 gate had one discord engine timing failure in the
full run, 66/0 alone, filed as #1553.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 14:49:48 -05:00

112 lines
7.6 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createServer, get } from 'node:http';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { startServer, boardURL, isLoopback } from '../src/serve.mjs';
import { fixture, close } from './fixture.mjs';
const post = (base, path, body, extra = {}) => fetch(base + path, { method: 'POST', headers: { 'content-type': 'application/json', ...extra }, body: typeof body === 'string' ? body : JSON.stringify(body) });
test('loopback host and board origin fail closed', async () => {
for (const h of ['127.0.0.1', '127.2.3.4', '::1', 'localhost']) assert.equal(isLoopback(h), true);
for (const h of ['0.0.0.0', '192.168.1.1', '::', 'example.com', '']) {
assert.equal(isLoopback(h), false);
await assert.rejects(startServer({ host: h, port: 0 }), /non-loopback/);
}
for (const u of ['https://127.0.0.1', 'http://example.com', 'http://127.0.0.1/path', 'http://user:[email protected]', 'http://127.0.0.1?x', 'http://127.0.0.1#x']) assert.throws(() => boardURL(u));
assert.equal(boardURL('http://localhost:7331'), 'http://127.0.0.1:7331');
assert.equal(boardURL('http://[::1]:7331'), 'http://[::1]:7331');
});
test('real board fixture passes through WebUI; assets and isolated seen/reply work', async () => {
const f = await fixture();
try {
assert.equal(f.web.address().address, '127.0.0.1');
const data = await (await fetch(f.base + '/api/board')).json();
assert.equal(data.sessions.length, 4);
assert.equal(data.sessions.filter(r => r.waitingOnYou).length, 2);
assert.deepEqual(data.counts, { working: 1, waiting: 1, error: 1, offline: 1, idle: 0, unknown: 0 });
const r = data.sessions.find(r => r.agent === 'agent1');
const seen = await post(f.base, '/api/seen', { project: r.project, agent: r.agent, lastActivity: r.lastActivity, seen: true });
assert.equal(seen.status, 200); assert.equal((await seen.json()).seen.length, 1);
const reply = await post(f.base, '/api/reply', { agent: 'proj/agent1', text: 'fixture reply' });
assert.equal((await reply.json()).delivered, true); assert.equal(f.captures.length, 1);
assert.match(f.captures[0].args.at(-1), /^fixture reply\n\(control-board:/);
for (const path of ['/', '/app.js', '/brand.js', '/console.css', '/live.css', '/shared/app.css', '/assets/fonts/manrope-400.woff2']) {
const response = await fetch(f.base + path); assert.equal(response.status, 200, path); assert.equal(response.headers.get('cache-control'), 'no-store');
}
// #1542: the shell's tokens, sprite, stylesheet and mono fonts, each with its type.
for (const [path, type] of [['/tokens.css', 'text/css'], ['/shell.css', 'text/css'], ['/icons.svg', 'image/svg+xml'], ...[400, 500, 700].map(w => [`/assets/fonts/jetbrains-mono-${w}.woff2`, 'font/woff2'])]) {
const response = await fetch(f.base + path); assert.equal(response.status, 200, path); assert.ok(response.headers.get('content-type').startsWith(type), path);
}
assert.equal((await fetch(f.base + '/../../AGENTS.md')).status, 404);
assert.equal((await fetch(f.base + '/api/reply')).status, 405);
assert.equal((await fetch(f.base + '/api/board', { method: 'OPTIONS' })).status, 405);
assert.equal((await post(f.base, '/api/reply', { agent: 'proj/agent1', text: 'blocked' }, { origin: 'https://evil.example' })).status, 403);
assert.equal(f.captures.length, 1);
const hostileHost = await new Promise((resolve, reject) => {
get(f.base + '/api/board', { headers: { host: 'evil.example' } }, res => { res.resume(); resolve(res.statusCode); }).on('error', reject);
});
assert.equal(hostileHost, 403);
} finally { await f.close(); }
});
test('proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect', async () => {
const requests = [];
const upstream = createServer(async (req, res) => {
let body = ''; for await (const c of req) body += c;
requests.push({ url: req.url, method: req.method, body });
if (req.url === '/api/board') { res.writeHead(302, { location: 'http://192.0.2.1/' }); return res.end('{}'); }
if (req.url.startsWith('/api/conversation')) { res.writeHead(404, { 'content-type': 'application/json' }); return res.end('{"error":"fixture unknown <x>","refusal":{"code":"unknown-branch","reconcile":true}}'); }
res.writeHead(409, { 'content-type': 'application/json' }); res.end('{"error":"fixture refusal <unsafe>"}');
});
await new Promise(r => upstream.listen(0, '127.0.0.1', r));
const web = await startServer({ port: 0, board: `http://127.0.0.1:${upstream.address().port}` });
const base = `http://127.0.0.1:${web.address().port}`;
try {
for (const path of ['/api/reply', '/api/seen']) {
const raw = '{ "agent": "proj/agent1", "text": "hello Ω", "seen": false }';
const res = await post(base, path, raw);
assert.equal(res.status, 409); assert.equal(await res.text(), '{"error":"fixture refusal <unsafe>"}');
assert.deepEqual(requests.at(-1), { url: path, method: 'POST', body: raw });
}
for (const raw of ['{', 'null', '[]', JSON.stringify({ text: 'x'.repeat(5000) })]) {
try { assert.equal((await post(base, '/api/reply', raw)).status, 400); } catch (err) { if (raw.length < 4096) throw err; }
}
assert.equal((await post(base, '/api/reply', '{}', { 'content-type': 'text/plain' })).status, 400);
assert.equal(requests.length, 2);
assert.equal((await fetch(base + '/api/board')).status, 502);
assert.equal(requests.length, 3);
// CHAT-02 conversation routes: GET only, the query passes unchanged, the board's status and body come back as sent.
for (const path of ['/api/conversations', '/api/conversation?id=pi-0a&branch=b.e5&cursor=c-1', '/api/conversation?id=x&id=y&unknown=%3C']) {
const res = await fetch(base + path);
assert.equal(res.status, 404, path);
assert.equal(await res.text(), '{"error":"fixture unknown <x>","refusal":{"code":"unknown-branch","reconcile":true}}');
assert.deepEqual(requests.at(-1), { url: path, method: 'GET', body: '' });
}
const before = requests.length;
assert.equal((await post(base, '/api/conversation?id=x', '{}')).status, 405);
assert.equal((await post(base, '/api/conversations', '{}')).status, 405);
assert.equal((await fetch(base + '/api/conversation?id=x', { headers: { origin: 'https://evil.example' } })).status, 403);
assert.equal(requests.length, before, 'refused before the board');
// Only the conversation routes carry a query upstream.
await fetch(base + '/api/board?x=1');
assert.equal(requests.at(-1).url, '/api/board');
} finally { await close(web); await close(upstream); }
});
test('unreachable board reports URL; CLI rejects unsupported options', async () => {
const probe = createServer(); await new Promise(r => probe.listen(0, '127.0.0.1', r));
const url = `http://127.0.0.1:${probe.address().port}`; await close(probe);
const web = await startServer({ port: 0, board: url, timeout: 100 });
try {
const res = await fetch(`http://127.0.0.1:${web.address().port}/api/board`);
assert.equal(res.status, 502); const result = await res.json(); assert.equal(result.board, url); assert.ok(result.error.includes(url));
} finally { await close(web); }
for (const args of [[], ['serve', '--port', 'abc'], ['serve', '--host', '0.0.0.0'], ['serve', '--board', 'http://example.com']]) {
const result = spawnSync(process.execPath, [fileURLToPath(new URL('../src/cli.mjs', import.meta.url)), ...args], { encoding: 'utf8' });
assert.equal(result.status, 2); assert.match(result.stderr, /^refused:/);
}
});