diff --git a/agents/researcher/work/queue-97/r3/base.txt b/agents/researcher/work/queue-97/r3/base.txt new file mode 100644 index 000000000..7ff83f215 --- /dev/null +++ b/agents/researcher/work/queue-97/r3/base.txt @@ -0,0 +1 @@ +cd976b09aca47f6506a1a8c27361fa35933a172b diff --git a/agents/researcher/work/queue-97/r3/build.md b/agents/researcher/work/queue-97/r3/build.md new file mode 100644 index 000000000..06d2a7dfb --- /dev/null +++ b/agents/researcher/work/queue-97/r3/build.md @@ -0,0 +1,57 @@ +# Row 97 (#1591) round 3: confirm-only rebase of the approved round 2 + +Owner: Researcher. Round 2 was approved by Darkwing and Filbert on manifest f5a11097 (base 6b0215aa). +Row 94 (2b2a9e14) then landed on `packages/webui/README.md`, so that file's hash no longer matches +any landing commit. This round changes no test, README sentence or mutant; it re-bases the same +change onto cd976b09 (origin/refactor, `base.txt`). + +## What moved + +6b0215aa..cd976b09 under `packages/webui`: + +- `README.md` +28/-2 (row 94): hunks at lines 276, 466 and 633 onward. My README hunk (lines 566 and + following in `build.patch` of round 2) is untouched, but its start line moved from 566 to 567. +- Also changed, none of them mine: `src/public/bus.js` (+38, a Messages view and a `messages` read), + `src/public/bus.css`, `src/serve.mjs`, and tests `bus-browser`, `messages`, `shell`, `views`. +- `board-held.test.mjs` and `browser-edge.test.mjs` are byte-identical on both bases. + +## Identical-hunk evidence + +In a fresh worktree at cd976b09, `git apply --3way agents/researcher/work/queue-97/r2/build.patch` +applied all three files cleanly. The `+` and `-` lines of the two patches (file headers excluded) +are 72 lines each and `cmp` reports them identical. The only difference between `r2/build.patch` and +`r3/build.patch` is one hunk header: `@@ -566,11 +566,19 @@` became `@@ -567,11 +567,19 @@`, plus +the context that follows. The manifest differs from round 2 in one line: + +| path | round 2 | round 3 | +|---|---|---| +| `packages/webui/README.md` | 01213bff... | ed0a1fe1... | +| `packages/webui/tests/board-held.test.mjs` | 32d01fba... | 32d01fba... (same) | +| `packages/webui/tests/browser-edge.test.mjs` | fc3cc8fa... | fc3cc8fa... (same) | + +No test or mutant changed, and none was run (per Sage). + +## Could an outcome change on the new base? Yes, for the browser tests + +`bus.js` is the file two of the mutants edit and the page the board-held test drives. Row 94's +change makes `render()` read `/api/bus/messages` before every view except Messages (swallowing a +non-sign-out error) and adds `messages` to the Board's `Promise.allSettled` reads. Two things this +could touch in my tests, neither checked by a run: + +1. `board-held.test.mjs` test 2 counts requests after sign-out and asserts none follow. Reading + `bus.js`, `render()` returns on `out()` at its first statement (line 696), before any of the + new `messages` reads, so I expect no change. I have not run it. +2. Request totals in both tests are compared with the count taken just before, not with a constant, + so extra reads in a render do not matter as long as none follow sign-out. + +What I did check, without running anything: each of the eight mutant anchors in `r2/mutants.py` +(including `bus.js` mutants 26 and 27) still matches exactly once in `app.js` and `bus.js` at +cd976b09. So the mutant runner works unchanged on this base. + +Suggested gate if Sage schedules one: the webui suite, then `r2/mutants.py` against the r3 worktree +for 27 and 26 (the two on `bus.js`). Sage's landing gate reruns webui in any case. + +## Packet + +`base.txt`, `build.patch` (`git diff cd976b09` of the three paths), `candidate-manifest.sha256` +(repo-relative paths, verifies against `/mnt/storage/scratch/researcher/q97-r3`). No secret markers. diff --git a/agents/researcher/work/queue-97/r3/build.patch b/agents/researcher/work/queue-97/r3/build.patch new file mode 100644 index 000000000..0d271dbf5 --- /dev/null +++ b/agents/researcher/work/queue-97/r3/build.patch @@ -0,0 +1,177 @@ +diff --git a/packages/webui/README.md b/packages/webui/README.md +index 69ed45d2a..07caedf3a 100644 +--- a/packages/webui/README.md ++++ b/packages/webui/README.md +@@ -567,11 +567,19 @@ 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, filed by the script that armed it, and assert that none is armed ++for the board while the toggle is off, even across Pause and Resume, and that ++one is armed once it is on. 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 19b875a8e..6815a4bf2 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,17 @@ 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 and bus.js both poll with a 10000 delay. A timer is filed by the script that armed it, read ++ // from the call stack, not from the shape of the callback, which either script may change. ++ const timeout = window.setTimeout.bind(window); ++ window.setTimeout = (f, ms, ...rest) => { ++ if (ms !== 10000) return timeout(f, ms, ...rest); ++ window.__polls.push(/\\/app\\.js[:?]/.test(new Error().stack) ? '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 +61,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 +71,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,11 +109,19 @@ 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'); + await wait(SESSIONS); + assert.equal(await shown('#board-view'), true); ++ // Positive control for the zero checks above and below: with the toggle on, the board does arm its timer. ++ await wait("window.__polls.includes('board')"); + assert.equal(await b.evaluate('localStorage.getItem("mosaic-console-board")'), '{"machine":true}'); + await b.navigate(`${base}?on`); + await wait(SESSIONS); +@@ -117,6 +139,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 +161,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 +177,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 446c9cc2e..efb261ff4 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/r3/candidate-manifest.sha256 b/agents/researcher/work/queue-97/r3/candidate-manifest.sha256 new file mode 100644 index 000000000..c3e385cf1 --- /dev/null +++ b/agents/researcher/work/queue-97/r3/candidate-manifest.sha256 @@ -0,0 +1,3 @@ +ed0a1fe1e7b73e6e7d5df19014373eee50c384f8f4386d30edd2db2cf678b9aa packages/webui/README.md +32d01fbac94fff0fed337aa47182f78a623eaf3ad5845a880a3dccfbe1c70586 packages/webui/tests/board-held.test.mjs +fc3cc8fa7388a6bae669bbaf60c21947f803620ff140af7e33d798a326716fc4 packages/webui/tests/browser-edge.test.mjs