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]>
210 lines
13 KiB
JavaScript
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(); }
|
|
});
|