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]>
This commit is contained in:
2026-10-12 18:12:57 -05:00
co-authored by Claude Sonnet 5.5
parent db27ca49fd
commit 81c2906c68
7 changed files with 432 additions and 0 deletions
+94
View File
@@ -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.
+168
View File
@@ -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');
@@ -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
@@ -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
@@ -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
@@ -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 <worktree> <out.txt> [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)
@@ -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