Files
stack/agents/researcher/work/queue-97/build.patch
T
jason.woltjeandClaude Sonnet 5.5 81c2906c68 researcher: row 97 build packet (#1591)
Packet commit, agents/researcher/work/queue-97 only. Secrets scan clean. Not pushed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-12 18:12:57 -05:00

169 lines
11 KiB
Diff

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');