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:
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user