diff --git a/agents/researcher/work/queue-97/build.md b/agents/researcher/work/queue-97/build.md new file mode 100644 index 000000000..ca3988bd5 --- /dev/null +++ b/agents/researcher/work/queue-97/build.md @@ -0,0 +1,94 @@ +# Row 97 (#1591): Row 95 follow-up, Board tests fail fast, README lists every redirect + +Owner: Researcher. Reviewers: Darkwing and Filbert. Brief: `docs/plans/2026-10-12_pm-test-fixes.md`, +"Row 95 follow-up". Built in a scratch worktree at base 3bd3f85c (a baseline worktree at the same +commit for the before column). Between 3bd3f85c and HEAD 646e3f77 nothing changed on the three +candidate paths. Nothing under `packages/webui/src/` changes; nothing goes live; no restart. + +## Candidate + +`candidate-manifest.sha256` lists three files, repo-relative, hashed from the candidate worktree. +`build.patch` is `git diff` of the same three against 3bd3f85c (sha256 +22150cee5aad48de22a041e78d7263d91f1f44959c78720b35393757b054ea24). + +- `packages/webui/tests/browser-edge.test.mjs` +- `packages/webui/tests/board-held.test.mjs` +- `packages/webui/README.md` + +## N1: the toggle step fails fast + +The wait for the first `/api/board` request after the toggle is raced against a 5 s timer. On a miss +the test throws "Show machine sessions made no /api/board request in 5 s", and the existing +`finally` closes the held responses, the browser and both servers. + +Proof, mutant 19 (toggle on reads nothing), `browser-edge.test.mjs` alone, 30 s limit: + +| | result | +|---|---| +| baseline | STALLED, no exit at 30 s | +| candidate | exit 1 in 5.8 s | + +After each run, no browser or node process was left (checked through /proc cwd links, 0 found). + +## N2: option taken, the test reaches a poll tick, plus the timers themselves + +Reaching a tick alone cannot kill mutants 13 and 27. After a tick, the guards downstream of +`schedule()` (`refresh()`'s `!machine`, `render()`'s `out()`) stop the fetch, so nothing the page +sends changes. What the mutants do change is whether the 10 s timer is armed. So the test does two +things: + +1. The page stub wraps `window.setTimeout`. A call with a delay of 10000 is recorded in + `window.__polls` ('board' for `refresh`, 'bus' otherwise), and in test 2 it is run after 150 ms. + The 600 ms settle in test 2 then spans several ticks, so the comment and README claim about polls + is true. +2. Both tests assert on the recorded timers. Test 1: none for the board with the toggle off, also + across Pause and Resume and after the `?off` reload, while the bus views do poll. Test 2: no new + timer after sign-out, including after a Pause and a Resume forced past `disabled`. The second + click is needed: the first only pauses, and the bus `schedule()` is reached with `paused()` false + only on the resume. + +Mutants (`mutants.py`, outputs `mut-base.txt` and `mut-candidate.txt`): + +| mutant | baseline | candidate | +|---|---|---| +| 13 schedule polls with the toggle off | ALIVE | killed | +| 27 bus schedule polls signed out | ALIVE | killed | +| 12 refresh reads with the toggle off | killed | killed | +| 26 bus render without its signed-out guard | killed | killed | +| 19 toggle on reads nothing | STALLED (30 s) | killed (5.8 s) | +| 14 refresh without its signed-out guard | ALIVE | ALIVE | + +Mutant 14 is an equivalent mutant and is not claimed: the guard in `refresh()` is redundant behind +`api()`, which already returns early when signed out, so no observable differs. It was alive in the +baseline for the same reason and is outside N2's list. Sources were restored byte for byte +(sha256 of `app.js` and `bus.js` checked at the end of both runs). + +## N3: the README + +The test paragraph now names the six redirected requests: the board (`/api/board`), `agents`, +`/api/conversations`, `/api/seen`, `/api/queue` and the send. It also says that the tests record the +10 s poll timers, that test 2's timers fire after 150 ms, and that `browser-edge.test.mjs` fails in +5 s if the toggle makes no board request. + +## Suites (candidate worktree, sequential, `suites.txt`) + +- webui `node --test tests/`: 101 pass, 0 fail. +- control-board `node --test tests/`: 124 pass, 0 fail. +- `scripts/test-auth.sh` 15/0, `test-conductor.sh` 17/0, `test-config.sh` 24/0, + `test-extension-package.sh` 18/0, `test-foundation.sh` 44/0, `test-queue.sh` 27/0, + `test-release.sh` 14/0, `test-task.sh` 98/0. +- `scripts/test-discord.sh`: 57 pass, 1 fail: "pi binary present at node_modules/.bin/pi for the + extension checks". The same single failure on the baseline worktree. A scratch worktree has no + `node_modules`; this is not caused by the candidate. Sage's gate rerun in the canonical checkout + is the real check. +- `test-queue.sh` skips its verify/render check because a worktree is not the queue's canonical root. + +## Secrets scan + +`build.patch`, `mutants.py`, the mutant outputs and `suites.txt` were scanned for token, secret, +password, api key and private key markers. The only match is a test title ("seeded secrets ..."). + +## Assumptions + +- Test 2's 150 ms is a test-only value; the 10 s in the product is untouched. +- The forced Pause click is a deliberate poke at a disabled control, commented in the test. diff --git a/agents/researcher/work/queue-97/build.patch b/agents/researcher/work/queue-97/build.patch new file mode 100644 index 000000000..f9dceb3a1 --- /dev/null +++ b/agents/researcher/work/queue-97/build.patch @@ -0,0 +1,168 @@ +diff --git a/packages/webui/README.md b/packages/webui/README.md +index 6656f1ff..c6664678 100644 +--- a/packages/webui/README.md ++++ b/packages/webui/README.md +@@ -566,11 +566,18 @@ claim that has an address and one that has none. They check the held roles + with the machine sessions off by default and no `/api/board` request, the + toggle on and off and its persistence across reloads, Message addressing + the send form to each role, no action without send, and that every fetch +-asked for `redirect: 'manual'`. Then a redirect answer on a board read, an +-`agents` read and a send: one line, no NetworkError, nothing stale left, no +-further request on a route change, Refresh or the toggle, and the draft +-kept. `send-browser.test.mjs` also checks the sign-out after its redirect +-receipt and that a reload works again. Since the held roles lead the Board, ++asked for `redirect: 'manual'`. The tests record every 10 s poll timer the ++page arms, and assert that none is armed for the board while the toggle is ++off, even across Pause and Resume. Then a redirect answer on each of six ++requests, the board read (`/api/board`), the `agents` read, `/api/conversations`, ++`/api/seen`, `/api/queue` and the send: one line, no NetworkError, nothing ++stale left, and no further request or new poll timer on a route change, ++Refresh, the toggle, a forced Pause click or a poll tick, and the draft kept. In ++that test the poll timers fire after 150 ms, so the check spans several ++ticks. `browser-edge.test.mjs` fails in 5 s, and closes the browser and ++servers, if the toggle makes no `/api/board` request. `send-browser.test.mjs` ++also checks the sign-out after its redirect receipt and that a reload works ++again. Since the held roles lead the Board, + the shell test's no-scroll check at 1440px uses a 1000px-tall window. + + Bus tests (`bus.test.mjs`, `bus-browser.test.mjs`) run the routes and the +diff --git a/packages/webui/tests/board-held.test.mjs b/packages/webui/tests/board-held.test.mjs +index 19b875a8..d4f81726 100644 +--- a/packages/webui/tests/board-held.test.mjs ++++ b/packages/webui/tests/board-held.test.mjs +@@ -5,7 +5,11 @@ + // 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). ++// under redirect: 'manual' (type opaqueredirect, status 0). It also records every 10 s poll timer ++// the page arms (window.__polls, 'board' for app.js and 'bus' for bus.js). The second test has the ++// timers fire after 150 ms, so its "no further request" check spans real poll ticks, and both ++// tests assert on the timers themselves: a guard that only the fetch behind it backs up would ++// otherwise change nothing the page sends. + import { test } from 'node:test'; + import assert from 'node:assert/strict'; + import { mkdtempSync, rmSync } from 'node:fs'; +@@ -29,9 +33,16 @@ const businesses = { + }, + }; + +-const STUB = `(() => { ++const stub = fast => `(() => { + const real = window.fetch.bind(window); +- window.__reqs = []; window.__answers = []; window.__redirect = []; ++ window.__reqs = []; window.__answers = []; window.__redirect = []; window.__polls = []; ++ // app.js polls with setTimeout(refresh, 10000) and bus.js with an arrow function; both use 10000. ++ const timeout = window.setTimeout.bind(window); ++ window.setTimeout = (f, ms, ...rest) => { ++ if (ms !== 10000) return timeout(f, ms, ...rest); ++ window.__polls.push(f.name === 'refresh' ? 'board' : 'bus'); ++ return timeout(f, ${fast} ? 150 : ms, ...rest); ++ }; + 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]; +@@ -49,7 +60,7 @@ const STUB = `(() => { + }; + })();`; + +-async function setup(machine) { ++async function setup(machine, fast = false) { + 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. +@@ -59,22 +70,24 @@ async function setup(machine) { + 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.call('Page.addScriptToEvaluateOnNewDocument', { source: stub(fast) }); + 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 polls = () => b.evaluate('window.__polls'); + 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 }; ++ return { b, base, wait, text, shown, reqs, polls, 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); ++ const { b, base, wait, text, shown, reqs, polls, settle, done } = await setup(false); ++ const boardPolls = async () => (await polls()).filter(p => p === 'board').length; + try { + // Off by default: the held roles show, the machine scan is never read. + await b.navigate(base); +@@ -95,6 +108,12 @@ test('the Board: held roles with no machine sessions, a claim with no address, t + await settle(); + assert.equal((await reqs()).some(r => r.path === '/api/board'), false); + assert.equal(await b.evaluate(`!!${SESSIONS}`), false); ++ // The board is not polled either: no timer for it, even after Pause and Resume, which re-arm one. ++ assert.equal(await boardPolls(), 0); ++ assert.ok((await polls()).includes('bus'), 'the bus views still poll'); ++ await b.evaluate('document.querySelector("#pause").click(), document.querySelector("#pause").click(), true'); ++ await settle(); ++ assert.equal(await boardPolls(), 0); + + // 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'); +@@ -117,6 +136,7 @@ test('the Board: held roles with no machine sessions, a claim with no address, t + assert.equal(await b.evaluate('document.querySelector("#show-machine").checked'), false); + await settle(); + assert.equal((await reqs()).some(r => r.path === '/api/board'), false); ++ assert.equal(await boardPolls(), 0); + + // Message opens the send form addressed to that role, with the caret in the message. + for (const role of ['coder', 'pm']) { +@@ -138,7 +158,8 @@ test('the Board: held roles with no machine sessions, a claim with no address, t + }); + + 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); ++ // The poll timers fire after 150 ms here, so the 600 ms settle below spans several ticks. ++ const { b, base, wait, text, shown, reqs, polls, settle, done } = await setup(true, true); + const LINE = 'Signed out. Reload the page to sign in.'; + const out = async () => { + await wait('!document.querySelector("#signed-out").hidden'); +@@ -153,12 +174,17 @@ test('a redirect answer on a Board read, a bus read, a conversation read, a boar + 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; ++ // No further Console fetch and no new poll timer: not on a route change, a Refresh, the toggle ++ // or a poll tick (a timer armed before the sign-out may still fire, and finds nothing to read). ++ const n = (await reqs()).length, armed = (await polls()).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'); ++ // Pause is disabled when signed out. Enabled by force for a pause and a resume, its handlers must still ++ // arm nothing: the guard in each schedule() is the only thing between the resume and a timer. ++ await b.evaluate('(() => { const p = document.querySelector("#pause"); p.disabled = false; p.click(); p.click(); p.disabled = true; return true; })()'); + await settle(); + assert.equal((await reqs()).length, n); ++ assert.equal((await polls()).length, armed); + assert.equal(await b.evaluate('!!document.querySelector("#bus-view h1")'), false); + assert.deepEqual((await reqs()).filter(r => r.redirect !== 'manual'), []); + }; +diff --git a/packages/webui/tests/browser-edge.test.mjs b/packages/webui/tests/browser-edge.test.mjs +index 446c9cc2..efb261ff 100644 +--- a/packages/webui/tests/browser-edge.test.mjs ++++ b/packages/webui/tests/browser-edge.test.mjs +@@ -26,7 +26,10 @@ test('browser edge states: loading, empty, malformed, stale, hostile/long values + assert.match(await b.evaluate('document.querySelector("#status").textContent'), /Machine sessions hidden/); + await b.evaluate('document.querySelector("#show-machine").click()'); + assert.match(await b.evaluate('document.querySelector("#status").textContent'), /Loading/); +- await boardRequested; ++ // Fails in 5 s if the toggle never reads the board; the finally below then closes the browser and servers. ++ let timer; ++ try { await Promise.race([boardRequested, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Show machine sessions made no /api/board request in 5 s')), 5000); })]); } ++ finally { clearTimeout(timer); } + holdBoard = false; boardResponse.end(JSON.stringify(index)); + await wait('document.querySelector("#sessions").textContent.includes("No sessions")'); + assert.equal(await b.evaluate('document.querySelector("#waiting-count").textContent'), '0'); diff --git a/agents/researcher/work/queue-97/candidate-manifest.sha256 b/agents/researcher/work/queue-97/candidate-manifest.sha256 new file mode 100644 index 000000000..1767653fe --- /dev/null +++ b/agents/researcher/work/queue-97/candidate-manifest.sha256 @@ -0,0 +1,3 @@ +5014e8bc86c0ec48ed30599d9511c26d511ffa0315a57b088d32fca7e8e33f89 packages/webui/README.md +44e66867b376e2e170fedc24b232960c09bba62219a5699d9bdb671d8a05e0e3 packages/webui/tests/board-held.test.mjs +fc3cc8fa7388a6bae669bbaf60c21947f803620ff140af7e33d798a326716fc4 packages/webui/tests/browser-edge.test.mjs diff --git a/agents/researcher/work/queue-97/mut-base.txt b/agents/researcher/work/queue-97/mut-base.txt new file mode 100644 index 000000000..ec5fe1a3a --- /dev/null +++ b/agents/researcher/work/queue-97/mut-base.txt @@ -0,0 +1,9 @@ +worktree /mnt/storage/scratch/researcher/q97-base +start 2026-10-12T22:58:46Z +13 schedule polls with the toggle off: ALIVE (exit 0, 9.0s, board-held.test.mjs browser-edge.test.mjs) +27 bus schedule polls signed out: ALIVE (exit 0, 8.3s, board-held.test.mjs browser-edge.test.mjs) +12 refresh reads with the toggle off: killed (exit 1, 6.7s, board-held.test.mjs browser-edge.test.mjs) +14 refresh without its signed-out guard: ALIVE (exit 0, 8.0s, board-held.test.mjs browser-edge.test.mjs) +26 bus render without its signed-out guard: killed (exit 1, 4.1s, board-held.test.mjs browser-edge.test.mjs) +19 toggle on reads nothing: STALLED (exit timeout, 30.0s, browser-edge.test.mjs) +killed 2 of 6; sources restored byte for byte: True diff --git a/agents/researcher/work/queue-97/mut-candidate.txt b/agents/researcher/work/queue-97/mut-candidate.txt new file mode 100644 index 000000000..4f0419862 --- /dev/null +++ b/agents/researcher/work/queue-97/mut-candidate.txt @@ -0,0 +1,9 @@ +worktree /mnt/storage/scratch/researcher/q97 +start 2026-10-12T23:03:49Z +13 schedule polls with the toggle off: killed (exit 1, 7.1s, board-held.test.mjs browser-edge.test.mjs) +27 bus schedule polls signed out: killed (exit 1, 4.5s, board-held.test.mjs browser-edge.test.mjs) +12 refresh reads with the toggle off: killed (exit 1, 7.0s, board-held.test.mjs browser-edge.test.mjs) +14 refresh without its signed-out guard: ALIVE (exit 0, 8.4s, board-held.test.mjs browser-edge.test.mjs) +26 bus render without its signed-out guard: killed (exit 1, 4.6s, board-held.test.mjs browser-edge.test.mjs) +19 toggle on reads nothing: killed (exit 1, 5.8s, browser-edge.test.mjs) +killed 5 of 6; sources restored byte for byte: True diff --git a/agents/researcher/work/queue-97/mutants.py b/agents/researcher/work/queue-97/mutants.py new file mode 100644 index 000000000..7a8823591 --- /dev/null +++ b/agents/researcher/work/queue-97/mutants.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# Row 97 (#1591) mutants. Each edit must match exactly once. Every run starts in its own process +# group and the group is killed when the run ends or times out, so no node or chromium is left +# behind. The mutated file is restored from the bytes read before the edit, and every touched +# file's sha256 is checked against its start at the end. +# Usage: mutants.py [timeout-seconds] +import hashlib, os, signal, subprocess, sys, time + +WT, OUT = sys.argv[1], sys.argv[2] +TIMEOUT = int(sys.argv[3]) if len(sys.argv) > 3 else 120 +A = 'packages/webui/src/public/app.js' +B = 'packages/webui/src/public/bus.js' +BOTH = ['tests/board-held.test.mjs', 'tests/browser-edge.test.mjs'] +EDGE = ['tests/browser-edge.test.mjs'] + +# (name, file, old, new, tests, per-run timeout) +M = [ + ('13 schedule polls with the toggle off', A, 'if (!paused && machine && !out()) timer', 'if (!paused && !out()) timer', BOTH, TIMEOUT), + ('27 bus schedule polls signed out', B, 'if (!paused() && !out()) timer', 'if (!paused()) timer', BOTH, TIMEOUT), + ('12 refresh reads with the toggle off', A, 'if (busy || !machine || out()) return;', 'if (busy || out()) return;', BOTH, TIMEOUT), + ('14 refresh without its signed-out guard', A, 'if (busy || !machine || out()) return;', 'if (busy || !machine) return;', BOTH, TIMEOUT), + ('26 bus render without its signed-out guard', B, " clearTimeout(timer);\n if (out()) return;\n", " clearTimeout(timer);\n", BOTH, TIMEOUT), + ('19 toggle on reads nothing', A, "fresh(); refresh(); } else hideMachine();", "fresh(); } else hideMachine();", EDGE, 30), +] + +env = dict(os.environ, TMPDIR=os.environ.get('TMPDIR', '/mnt/storage/scratch/tmp')) +sha = lambda p: hashlib.sha256(open(os.path.join(WT, p), 'rb').read()).hexdigest() +start = {p: sha(p) for p in (A, B)} + +def run(tests, limit): + t0 = time.time() + p = subprocess.Popen(['node', '--test', *tests], cwd=os.path.join(WT, 'packages/webui'), env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, text=True) + try: + out, _ = p.communicate(timeout=limit) + code = p.returncode + except subprocess.TimeoutExpired: + code, out = 'timeout', '' + finally: + try: os.killpg(p.pid, signal.SIGKILL) + except ProcessLookupError: pass + try: p.communicate(timeout=5) + except Exception: pass + return code, round(time.time() - t0, 1) + +with open(OUT, 'w') as f: + def say(s): print(s); f.write(s + '\n'); f.flush() + say(f'worktree {WT}') + say(f'start {time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}') + killed = 0 + for name, path, old, new, tests, limit in M: + full = os.path.join(WT, path) + orig = open(full, 'rb').read() + src = orig.decode() + if src.count(old) != 1: + say(f'{name}: NO MATCH ({src.count(old)}), skipped'); continue + try: + open(full, 'w').write(src.replace(old, new)) + code, secs = run(tests, limit) + finally: + open(full, 'wb').write(orig) + # A timeout is a stall, not a kill: the file never exited by itself. + dead = code not in (0, 'timeout') + killed += dead + verdict = 'killed' if dead else 'STALLED' if code == 'timeout' else 'ALIVE' + say(f'{name}: {verdict} (exit {code}, {secs}s, {" ".join(t.split("/")[-1] for t in tests)})') + ok = all(sha(p) == start[p] for p in (A, B)) + say(f'killed {killed} of {len(M)}; sources restored byte for byte: {ok}') + sys.exit(0 if ok else 1) diff --git a/agents/researcher/work/queue-97/suites.txt b/agents/researcher/work/queue-97/suites.txt new file mode 100644 index 000000000..d2866564b --- /dev/null +++ b/agents/researcher/work/queue-97/suites.txt @@ -0,0 +1,80 @@ +== packages/webui 23:06:57Z +✔ JetBrains Mono files match the digests in their sources file, with the OFL text (4.643801ms) +✔ shipped strings are unchanged, and the web keeps its write boundary (5.776889ms) +✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (8847.454714ms) +✔ five states on Board, Inbox, Tasks, Agents and Trail (2494.519419ms) +✔ Queue, Business and Settings in their five states, with no move button (6464.178105ms) +✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1552.380439ms) +✔ the server log names no config path when the system config refuses (315.75153ms) +ℹ tests 101 +ℹ suites 0 +ℹ pass 101 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 44978.761927 +exit 0 +== packages/control-board 23:07:42Z +✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (4.621582ms) +✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.556016ms) +✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (10.855564ms) +✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (37.384796ms) +✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (7.213887ms) +✔ every refusal code the reader can raise has an HTTP status (1.357353ms) +✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (58.439944ms) +ℹ tests 124 +ℹ suites 0 +ℹ pass 124 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 822.284748 +exit 0 +== scripts/test-auth.sh 23:07:43Z +exit 0 +OK auth.sh without valid config refuses + +selftest: 15 passed, 0 failed +== scripts/test-conductor.sh 23:07:44Z +exit 0 +OK invalid policy exits 2 (exit 2) + +selftest: 17 passed, 0 failed +== scripts/test-config.sh 23:07:50Z +exit 0 +OK failed validation modified nothing + +selftest: 24 passed, 0 failed +== scripts/test-discord.sh 23:07:52Z +exit 1 +OK service install with USER unset finishes and names the account for lingering + +discord suite: 57 passed, 1 failed +== scripts/test-extension-package.sh 23:07:57Z +exit 0 +OK nested second entrypoint fails closed + +extension package selftest: 18 passed, 0 failed +== scripts/test-foundation.sh 23:07:59Z +exit 0 +OK no field of the non-bundle file is echoed + +selftest: 44 passed, 0 failed +== scripts/test-queue.sh 23:08:45Z +exit 0 +skip queue verify and render --check: this checkout (/mnt/storage/scratch/researcher/q97) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + +queue suite: 27 passed, 0 failed +== scripts/test-release.sh 23:09:14Z +exit 0 +OK rollback without previous refuses (exit 1) + +selftest: 14 passed, 0 failed +== scripts/test-task.sh 23:09:25Z +exit 0 +OK no agent identity on headless run + +selftest: 98 passed, 0 failed +== done 23:10:00Z