133 lines
9.8 KiB
JavaScript
133 lines
9.8 KiB
JavaScript
// Row 278: real Console rendering and HTTP with mutable bus-answer stand-ins.
|
|
// Broker claim/release semantics are covered by row 277; this tests the reader.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { BusReadError } from '../src/bus.mjs';
|
|
import { fixture, close } from './fixture.mjs';
|
|
import { browser } from './browser.mjs';
|
|
import { startTesterSocket, TESTER_ORIGIN } from '../src/tester-socket.mjs';
|
|
import { testerBrowser } from './tester-browser-fixture.mjs';
|
|
import { tmp } from '../../cli/tests/helpers.mjs';
|
|
|
|
const AT = '2026-01-01T00:00:00Z';
|
|
const THREAD = '11111111-1111-4111-8111-111111111111';
|
|
const claim = { thread: THREAD, holder_run: 'thread-run', harness: 'pi', at: AT };
|
|
async function setup(t) {
|
|
const f = await fixture(); t.after(() => f.close());
|
|
const state = { rows: [], failure: null, offline: false, calls: [], hold: false, release: null };
|
|
t.after(() => { state.hold = false; state.release?.(); });
|
|
const role = { role: 'coder', holder_run: 'role-run', harness: 'pi', at: AT, state: 'idle', inputNeeded: [] };
|
|
const read = (verb, answer) => async () => {
|
|
state.calls.push(verb);
|
|
if (state.offline) throw new BusReadError('outcome-unknown');
|
|
if (verb === 'thread.claims' && state.failure) throw new BusReadError(state.failure);
|
|
if (verb === 'thread.claims' && state.hold) await new Promise(resolve => { state.release = resolve; });
|
|
return typeof answer === 'function' ? answer() : answer;
|
|
};
|
|
const bus = { agents: read('agents', [role]), inbox: read('inbox', []), messages: read('messages', { messages: [], unread: 0 }), 'thread.claims': read('thread.claims', () => state.rows) };
|
|
const web = await startServer({ port: 0, board: f.boardURL, bus }); t.after(() => close(web));
|
|
const b = await browser(); t.after(() => b.close());
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,30));}throw Error(${JSON.stringify(expression)})})()`);
|
|
await b.navigate(`http://127.0.0.1:${web.address().port}/#/agents`);
|
|
await wait(`!!document.querySelector('#b-thread-claims')`);
|
|
const roleHtml = () => b.evaluate(`document.querySelector('[aria-labelledby="b-claims"]').outerHTML`);
|
|
const refresh = () => b.evaluate(`document.querySelector('#refresh').click(),true`);
|
|
return { b, state, wait, roleHtml, refresh };
|
|
}
|
|
|
|
test('row278 browser lists held claims and release preserves role rows and totals', async t => {
|
|
const { b, state, wait, roleHtml, refresh } = await setup(t);
|
|
const before = await roleHtml();
|
|
assert.match(await b.evaluate(`document.querySelector('[aria-labelledby="b-thread-claims"]').textContent`), /No thread claim is held/);
|
|
state.rows = [claim]; await refresh();
|
|
await wait(`document.querySelector('.s1-thread-claims tbody')?.textContent.includes('${THREAD}')`);
|
|
assert.deepEqual(await b.evaluate(`Array.from(document.querySelectorAll('.s1-thread-claims thead th'),e=>e.textContent)`), ['Thread', 'Holder run', 'Harness', 'Claimed at']);
|
|
assert.equal(await b.evaluate(`document.querySelector('#b-claims .count').textContent`), '1');
|
|
assert.equal(await roleHtml(), before);
|
|
assert.equal(await b.evaluate(`document.querySelector('.s1-thread-claims time').dateTime`), AT);
|
|
state.rows = []; await refresh();
|
|
await wait(`document.querySelector('[aria-labelledby="b-thread-claims"]').textContent.includes('No thread claim is held')`);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.s1-thread-claims tbody tr').length`), 0);
|
|
assert.equal(await roleHtml(), before);
|
|
});
|
|
|
|
test('row278 browser claims 403 stays local and role section remains byte-equal', async t => {
|
|
const { b, state, wait, roleHtml, refresh } = await setup(t);
|
|
const before = await roleHtml(); state.rows = [claim]; await refresh();
|
|
await wait(`!!document.querySelector('.s1-thread-claims tbody tr')`);
|
|
for (const code of ['human-required', 'not-configured', 'outcome-unknown']) {
|
|
state.failure = code; await refresh();
|
|
await wait(`document.querySelector('[aria-labelledby="b-thread-claims"] code')?.textContent===${JSON.stringify(code)}`);
|
|
assert.equal(await roleHtml(), before, code);
|
|
assert.equal(await b.evaluate(`document.querySelector('#b-claims .count').textContent`), '1');
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.s1-thread-claims tbody tr').length`), 0, 'failed read drops claims only');
|
|
}
|
|
state.offline = true; await refresh();
|
|
await wait(`!!document.querySelector('.s1-stale') && document.querySelector('[aria-labelledby="b-thread-claims"] code')?.textContent==='not-read'`);
|
|
assert.equal(await roleHtml(), before, 'other reads may use the kept role section');
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.s1-thread-claims tbody tr').length`), 0, 'a later stale repaint must not recover refused claims');
|
|
});
|
|
|
|
test('row278 browser renders only four escaped claim fields and never instance or other extras', async t => {
|
|
const { b, state, wait, refresh } = await setup(t);
|
|
const hostile = '<img src=x onerror="window.claimInjected=1">';
|
|
state.rows = [{ ...claim, thread: hostile, holder_run: '<script>window.claimInjected=2</script>', harness: '<b>pi</b>',
|
|
instance: 'NEVER_INSTANCE', name: 'NEVER_THREAD_NAME', body: 'NEVER_MESSAGE_BODY', address: 'NEVER_ADDRESS' }];
|
|
await refresh(); await wait(`document.querySelector('.s1-thread-claims tbody')?.textContent.includes('onerror')`);
|
|
const text = await b.evaluate(`document.querySelector('[aria-labelledby="b-thread-claims"]').textContent`);
|
|
assert.ok(text.includes(hostile)); assert.ok(text.includes('<script>window.claimInjected=2</script>')); assert.ok(text.includes('<b>pi</b>'));
|
|
assert.doesNotMatch(text, /NEVER_INSTANCE|NEVER_THREAD_NAME|NEVER_MESSAGE_BODY|NEVER_ADDRESS/);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.s1-thread-claims tbody tr > *').length`), 4);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.s1-thread-claims script,.s1-thread-claims img,.s1-thread-claims b').length`), 0);
|
|
assert.equal(await b.evaluate(`!!window.claimInjected`), false);
|
|
});
|
|
|
|
test('row278 browser sign-out removes claims and prevents reuse of cached answers', async t => {
|
|
const { b, state, wait, refresh } = await setup(t);
|
|
state.rows = [claim]; await refresh(); await wait(`!!document.querySelector('.s1-thread-claims tbody tr')`);
|
|
await b.evaluate(`window.claimFetch=fetch;window.fetch=(url,options)=>url==='/api/bus/thread.claims'?Promise.resolve(new Response(JSON.stringify({error:'sign-in required'}),{status:401})):window.claimFetch(url,options);true`);
|
|
await refresh();
|
|
await wait(`document.body.classList.contains('signed-out') && !document.querySelector('#b-thread-claims')`);
|
|
const calls = state.calls.length; await refresh(); assert.equal(state.calls.length, calls);
|
|
// Simulate returning to the route while the bus is unavailable. A cache that
|
|
// survived sign-out would repopulate the held thread from reader(false).
|
|
state.offline = true;
|
|
await b.evaluate(`window.fetch=window.claimFetch;document.body.classList.remove('signed-out');location.hash='#/agents?after-signout=1';true`);
|
|
await wait(`document.querySelector('#bus-view h1')?.textContent==='The read failed'`);
|
|
assert.doesNotMatch(await b.evaluate(`document.querySelector('#bus-view').textContent`), new RegExp(THREAD));
|
|
});
|
|
|
|
test('row278 tester browser keeps role rows without requesting the owner claims section', async t => {
|
|
const dataRoot = tmp(t, 'claims-view-'), calls = [], reads = [];
|
|
const web = await startTesterSocket({ dataRoot, board: 'http://127.0.0.1:9', bus: {
|
|
agents: async () => [{ role: 'coder', holder_run: 'role-run', harness: 'pi', at: AT }],
|
|
messages: async () => ({ messages: [], unread: 0 }),
|
|
'thread.claims': async () => { calls.push('thread.claims'); return [claim]; },
|
|
} });
|
|
t.after(() => close(web));
|
|
const b = await testerBrowser(web); t.after(() => b.close());
|
|
b.on('Fetch.requestPaused', ({ request }) => { if (new URL(request.url).pathname === '/api/bus/thread.claims') reads.push(request.url); });
|
|
await b.navigate(TESTER_ORIGIN + '/#/agents');
|
|
await b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(document.querySelector('#tester-banner')&&document.querySelector('.s1-agents tbody tr'))return true;await new Promise(r=>setTimeout(r,30));}throw Error('tester Agents view did not render')})()`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#b-claims .count').textContent`), '1');
|
|
assert.equal(await b.evaluate(`document.querySelector('#b-thread-claims')===null`), true);
|
|
assert.deepEqual(calls, []);
|
|
assert.deepEqual(reads, []);
|
|
});
|
|
|
|
test('row278 browser discards a late claims answer after sign-out', async t => {
|
|
const { b, state, wait, refresh } = await setup(t);
|
|
await b.evaluate(`window.claimFetch=fetch;window.fetch=async(url,options)=>{const r=await window.claimFetch(url,options);if(url==='/api/bus/thread.claims'){const json=r.json.bind(r);r.json=async()=>{const body=await json();window.lateClaimsFinished=true;return body;};}return r;};true`);
|
|
state.rows = [claim]; state.hold = true; await refresh();
|
|
for (let n = 0; !state.release && n < 100; n++) await new Promise(resolve => setTimeout(resolve, 10));
|
|
assert.equal(typeof state.release, 'function');
|
|
await b.evaluate(`document.dispatchEvent(new Event('mosaic-signed-out')),true`);
|
|
state.release(); state.hold = false;
|
|
await wait(`window.lateClaimsFinished && document.body.classList.contains('signed-out') && !document.querySelector('#b-thread-claims')`);
|
|
state.offline = true;
|
|
await b.evaluate(`window.fetch=window.claimFetch;document.body.classList.remove('signed-out');location.hash='#/agents?late-answer=1';true`);
|
|
await wait(`document.querySelector('#bus-view h1')?.textContent==='The read failed'`);
|
|
assert.doesNotMatch(await b.evaluate(`document.querySelector('#bus-view').textContent`), new RegExp(THREAD));
|
|
});
|