Files
stack/packages/webui/tests/board-held.test.mjs
T
jason.woltjeandClaude Opus 5.5 17ad6f35e2 webui: the Console's Board shows held roles, machine sessions behind a toggle, a signed-out line (#1589)
Row 95, round 1. The Board's first section lists each role a run holds
on the bus (from /api/bus/agents), with a Message button on a Console
started with --send. Machine sessions sit behind a "Show machine
sessions" checkbox, off by default, kept in localStorage; while off the
page never reads /api/board. Every Console fetch passes
redirect: 'manual'; the first opaqueredirect signs the page out: timers
stop, reads are dropped, one line says to reload, and the send form
keeps its draft.

Candidate manifest 246c0557 (agents/filbert/work/queue-95/), base
a3f6e9e6, no drift on the manifest paths. Builder Filbert. Approved by
Darkwing (comment 27953). Mutants 30/35 killed, 5 redundant guards.
Sage's gate 2026-10-12T21:52:13Z-21:54:35Z: webui 86/0, control-board
124/0, every scripts/test-*.sh green.

Live on landing: serve.mjs reads src/public on every request with
no-store (decision 123). Mos gave the go (sage-0115).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 16:56:43 -05:00

210 lines
13 KiB
JavaScript

// Row 95 (#1589): the Board leads with the stack's held roles, from the bus `agents` read, and
// keeps the machine scan's sessions behind "Show machine sessions", off by default and kept in
// the page's settings. A redirect answer on any Console fetch is the sign-in proxy's: the page
// stops and says, in one line, to reload and sign in. The roles come from an in-process broker
// through its reader session. A script added before the page loads records every fetch the page
// makes, answers /api/config with send on (unless the URL has ?nosend), answers POST
// /api/bus/message from a queue, and answers any path in window.__redirect the way fetch does
// under redirect: 'manual' (type opaqueredirect, status 0).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../../bus/src/store.mjs';
import { Broker } from '../../bus/src/broker.mjs';
import { views } from '../../bus/src/views.mjs';
import { startServer } from '../src/serve.mjs';
import { browser } from './browser.mjs';
import { fixture, close } from './fixture.mjs';
const businesses = {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' },
roles: {
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
cto: { authority: { withinRole: ['message.send'], crossRole: [] } },
coder: { authority: { withinRole: ['message.send'], crossRole: [] } },
},
},
};
const STUB = `(() => {
const real = window.fetch.bind(window);
window.__reqs = []; window.__answers = []; window.__redirect = [];
const redirect = () => ({ type: 'opaqueredirect', ok: false, status: 0, json: async () => { throw new SyntaxError('no body'); } });
window.fetch = async (url, init = {}) => {
const u = String(url), path = u.split('?')[0];
window.__reqs.push({ path, redirect: init.redirect ?? null });
if (window.__redirect.includes(path)) return redirect();
if (path === '/api/config' && !location.search.includes('nosend')) {
const c = await (await real(url, init)).json();
return new Response(JSON.stringify({ ...c, send: true }), { headers: { 'content-type': 'application/json' } });
}
if (path !== '/api/bus/message') return real(url, init);
const a = window.__answers.shift();
if (!a) throw new TypeError('no answer queued');
if (a.redirect) return redirect();
return new Response(JSON.stringify(a.body), { status: a.status, headers: { 'content-type': 'application/json' } });
};
})();`;
async function setup(machine) {
const root = mkdtempSync(join(tmpdir(), 'webui-board-held-'));
const store = new Store(root), b0 = new Broker({ store, businesses });
// pm's run has an address; coder's claim has none, which the Board still lists.
b0.request(b0.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi', address: 'pm-run' }), { verb: 'role.claim' });
b0.request(b0.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'claude-code' }), { verb: 'role.claim' });
const reader = b0.bindReader({ business: 'demo' });
const bus = views({ call: async (verb, args = {}) => structuredClone(b0.request(reader, { verb, args })) });
const f = await fixture(), b = await browser({ machine });
const web = await startServer({ port: 0, board: f.boardURL, bus }), base = `http://127.0.0.1:${web.address().port}/`;
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: STUB });
await b.viewport(1280);
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,50))}throw new Error('Condition timed out: '+${JSON.stringify(expr)})})()`);
const text = sel => b.evaluate(`document.querySelector(${JSON.stringify(sel)})?.textContent ?? null`);
const shown = sel => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(sel)});return !!e && !e.hidden && getComputedStyle(e).display !== 'none' && !!e.getClientRects().length})()`);
const reqs = () => b.evaluate('window.__reqs');
const settle = () => new Promise(r => setTimeout(r, 600));
const done = async () => { await b.close(); await close(web); await f.close(); store.close(); rmSync(root, { recursive: true, force: true }); };
return { b, base, wait, text, shown, reqs, settle, done };
}
const HELD = 'document.querySelectorAll("#held table tbody tr").length === 2';
const SESSIONS = 'document.querySelector("table.sessions [data-open=\'proj/agent1\']")';
test('the Board: held roles with no machine sessions, a claim with no address, the toggle kept across a reload, and Message addressed to the role', { timeout: 120000 }, async () => {
const { b, base, wait, text, shown, reqs, settle, done } = await setup(false);
try {
// Off by default: the held roles show, the machine scan is never read.
await b.navigate(base);
await wait(HELD);
const rows = await b.evaluate('[...document.querySelectorAll("#held table tbody tr")].map(r => [...r.cells].map(c => c.textContent))');
const byRole = Object.fromEntries(rows.map(r => [r[0], r]));
assert.deepEqual(Object.keys(byRole).sort(), ['coder', 'pm']);
assert.equal(byRole.coder[3], 'no address');
assert.equal(byRole.pm[3], 'pm-run');
assert.match(byRole.coder[4], /^coder-run/);
assert.match(byRole.coder[1], /idle|waiting|working|unknown/);
assert.equal(await text('#held-count'), '2');
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#held [data-message-role]")].map(e => e.getAttribute("aria-label")).sort()'), ['Message coder', 'Message pm']);
assert.equal(await b.evaluate('document.querySelector("#show-machine").checked'), false);
assert.match(await text('#status'), /^Machine sessions hidden\./);
assert.equal(await shown('#board-view'), false);
assert.equal(await shown('#board-tree'), false);
await settle();
assert.equal((await reqs()).some(r => r.path === '/api/board'), false);
assert.equal(await b.evaluate(`!!${SESSIONS}`), false);
// On: the machine sessions are read and shown, and the choice is kept for the next load.
await b.evaluate('document.querySelector("#show-machine").click(), true');
await wait(SESSIONS);
assert.equal(await shown('#board-view'), true);
assert.equal(await b.evaluate('localStorage.getItem("mosaic-console-board")'), '{"machine":true}');
await b.navigate(`${base}?on`);
await wait(SESSIONS);
assert.equal(await b.evaluate('document.querySelector("#show-machine").checked'), true);
await wait(HELD);
// Off again: the sessions go, the held roles stay, and the next load reads no board.
await b.evaluate('document.querySelector("#show-machine").click(), true');
await wait(`!${SESSIONS}`);
assert.equal(await shown('#board-view'), false);
assert.equal(await shown('#held-section'), true);
assert.equal(await b.evaluate('localStorage.getItem("mosaic-console-board")'), '{"machine":false}');
await b.navigate(`${base}?off`);
await wait(HELD);
assert.equal(await b.evaluate('document.querySelector("#show-machine").checked'), false);
await settle();
assert.equal((await reqs()).some(r => r.path === '/api/board'), false);
// Message opens the send form addressed to that role, with the caret in the message.
for (const role of ['coder', 'pm']) {
await b.evaluate(`location.hash = '#/', true`);
await wait(HELD);
await b.evaluate(`document.querySelector('#held [data-message-role="${role}"]').click(), true`);
await wait(`location.hash === '#/inbox' && document.querySelector('#send-to')?.value === '${role}' && document.activeElement === document.querySelector('#send-body')`);
assert.equal(await shown('#held-section'), false);
}
// Without send there is no Message action; the roles still show.
await b.navigate(`${base}?nosend`);
await wait(HELD);
assert.equal(await b.evaluate('document.querySelectorAll("#held [data-message-role]").length'), 0);
assert.match(await text('#held'), /--send/);
// Every Console fetch the page made asked for redirect: 'manual'.
assert.deepEqual((await reqs()).filter(r => r.redirect !== 'manual'), []);
} finally { await done(); }
});
test('a redirect answer on a Board read, a bus read, a conversation read, a board POST, a module read and a send signs the page out: one line, nothing stale, no further reads', { timeout: 120000 }, async () => {
const { b, base, wait, text, shown, reqs, settle, done } = await setup(true);
const LINE = 'Signed out. Reload the page to sign in.';
const out = async () => {
await wait('!document.querySelector("#signed-out").hidden');
assert.equal(await text('#signed-out'), LINE);
assert.equal(await b.evaluate('document.activeElement === document.querySelector("#signed-out") || document.activeElement?.closest?.("#send-section") !== null'), true);
const page = await b.evaluate('document.body.innerText');
// No failure text from the redirected read either: the footer stays visible when signed out.
assert.doesNotMatch(page, /NetworkError|Failed to fetch|did not answer|Board unreachable|[Nn]ot read|last known data/);
assert.equal(page.split(LINE).length, 2);
// Nothing read before stays on the page as if current.
assert.equal(await b.evaluate(`!!${SESSIONS}`), false);
assert.equal(await b.evaluate('document.querySelector("#held").children.length'), 0);
for (const sel of ['#held-section', '#board-view', '#board-head', '#error']) assert.equal(await shown(sel), false, sel);
assert.equal(await b.evaluate('document.querySelector("#refresh").disabled'), true);
// No further Console fetch: not on a route change, a Refresh, the toggle or the poll.
const n = (await reqs()).length;
await b.evaluate(`location.hash = location.hash === '#/agents' ? '#/inbox' : '#/agents', true`);
await b.evaluate('document.querySelector("#refresh").click(), document.querySelector("#show-machine").click(), document.querySelector("#show-machine").click(), true');
await settle();
assert.equal((await reqs()).length, n);
assert.equal(await b.evaluate('!!document.querySelector("#bus-view h1")'), false);
assert.deepEqual((await reqs()).filter(r => r.redirect !== 'manual'), []);
};
try {
// A Board read: the machine scan's /api/board, after a good first read.
await b.navigate(base);
await wait(`${SESSIONS} && ${HELD}`);
await b.evaluate(`window.__redirect.push('/api/board'), document.querySelector('#refresh').click(), true`);
await out();
// A bus read: the held roles' agents read, on a later Refresh.
await b.navigate(`${base}?bus`);
await wait(`${SESSIONS} && ${HELD}`);
assert.equal(await b.evaluate('document.querySelector("#signed-out").hidden'), true);
await b.evaluate(`window.__redirect.push('/api/bus/agents'), document.querySelector('#refresh').click(), true`);
await out();
// A conversation read: History on a session asks for the catalogue.
await b.navigate(`${base}?conv`);
await wait(`${SESSIONS} && ${HELD}`);
await b.evaluate(`window.__redirect.push('/api/conversations'), document.querySelector('table.sessions [data-history="proj/agent1"]').click(), true`);
await out();
// A board POST: Seen in the inspector posts /api/seen.
await b.navigate(`${base}?seen`);
await wait(`${SESSIONS} && ${HELD}`);
await b.evaluate(`document.querySelector('table.sessions [data-open="proj/agent1"]').click(), true`);
await wait('document.querySelector("#inspection [data-seen]")');
await b.evaluate(`window.__redirect.push('/api/seen'), document.querySelector('#inspection [data-seen]').click(), true`);
await out();
// A module read: the Queue page's /api/queue.
await b.navigate(`${base}?queue`);
await wait(`${SESSIONS} && ${HELD}`);
await b.evaluate(`window.__redirect.push('/api/queue'), location.hash = '#/queue', true`);
await out();
// A send: the receipt says it was not sent and keeps the draft; the form stays for the draft.
await b.navigate(`${base}?send#/inbox`);
await wait('document.querySelector("#send-to option[value=coder]") && document.querySelector("#send-form [data-send]")');
await b.evaluate(`(()=>{const t=document.querySelector('#send-to'),m=document.querySelector('#send-body');t.value='coder';t.dispatchEvent(new Event('input',{bubbles:true}));m.value='Kept draft';m.dispatchEvent(new Event('input',{bubbles:true}));window.__answers.push({redirect:true});document.querySelector('#send-form [data-send]').click();return true})()`);
await wait('document.querySelector("#send-section .receipt")');
assert.match(await text('#send-section .receipt'), /^Not sent to coder: the sign-in proxy redirected it, so it never reached the Console; reload the page to sign in again \(sign-in\)\. The draft is kept\./);
assert.equal(await b.evaluate('document.querySelector("#send-body").value'), 'Kept draft');
assert.equal(await shown('#send-section'), true);
await out();
} finally { await done(); }
});