Review of #1543 candidate dba2429e: gate green with Docker, 31 mutants (26 killed), Node, import and Chromium probes. Posted as comment 27186. Co-Authored-By: Claude Opus 5.5 <[email protected]>
13 KiB
Row 54, read-only console views, round 1 review (Darkwing)
Issue #1543, request comment 27182, packet comment 27183. Base
d64f434f. Candidate manifest sha256
dba2429e411ae22b1e939344247e9f3857a48e488dc7da11c578d1092288ea8a, 14
files. The packet manifest and my snapshot of the checkout both check 14
OK, and row54.patch applied at c6b214e5 gives the same 14 files.
c6b214e5 is d64f434f plus queue revs 373 to 375, which touch only
docs/plans/queue.json and QUEUE.md.
Verdict: changes. The four fixes Sage asked me to check are right, and
the tests pin each one. Every suite is green. Two things block. The
Business view shows "none" for Arbiters on every real business file, and a
queue child that fails at import sends an absolute file:// path and a
stack trace to the browser.
Method
- Detached worktrees
papplyandmutwtatc6b214e5withrow54.patchapplied and staged. Both check 14 OK.mutwtstill checks 14 OK after the mutant runs (r1/mut/manifest-after.txt). r1/gate.sh: thepackages/webui,packages/queueandpackages/businessnode suites,build-tokens.mjs --check, then everyscripts/test-*.shwithDOCKER_HOST=unix:///nonexistent.sock, 20:05:27Z to 20:08:51Z. I skipped thepackages/clinode suite:packages/clidoesn't change and is frozen.- 31 mutants (
r1/mut/mutate.py,r1/mut/run.sh), each against the fullpackages/webuisuite, except D31 againstpackages/queue, 20:09:04Z to 20:37:00Z. test-releasewith Docker.- Node probes (
r1/probes/probe-node.mjs, outputprobe-node.txt): the redactor on hostile strings (P2), the four routes under bad Host, bad Origin, HEAD, OPTIONS and odd ids (P3), the queue child against a fake script that hangs, floods, refuses, prints the wrong shape or dies (P4), and the bus host state file (P5). - An import probe (
r1/probes/probe-import.mjs, outputprobe-import.txt): the realqueue-read.mjsbeside astore.mjswith a syntax error, and beside none (P4f). - Browser probes in Chromium through the candidate's
tests/browser.mjson the seeded fixture with the real reads (r1/probes/probe-browser.test.mjs, outputprobe-browser.txt): B1 to B4. - Read every changed file against the brief, decision 83 and my row 53 notes.
Node v26.8.1, TMPDIR=~/darkwing-scratch/r54a/tmp. No tracker request.
Suites
| Suite | Result |
|---|---|
| packages/webui (node) | 38/0 |
| packages/queue (node) | 149/0 |
| packages/business (node) | 60/0 |
| build-tokens --check | rc 0, "tokens.css is current" |
| test-auth | 15/0 |
| test-conductor | 17/0 |
| test-config | 24/0 |
| test-discord | 66/0 |
| test-extension-package | 18/0 |
| test-foundation | 44/0 |
| test-queue | 27/0 |
| test-release | 4/0 without Docker, 14/0 with it |
| test-task, Docker unreachable | 26/0 |
I didn't run test-task with real Docker, because it makes live model calls.
The four fixes
| Item | Result |
|---|---|
| Refresh timer during a navigation | Fixed. clearTimeout(timer) is the first line of render(). D01 drops it and dies at "the navigation cleared the due refresh" (views.test.mjs:150) |
| H1 focus on a same-page refresh | Fixed. B3: focus on the Queue H1, fire the 10 s refresh, focus is still on the new H1. D02 and D03 die (table below) |
Stale #conv-pick |
Fixed. openConversation() empties it beside #conv-meta. D04 dies at "no stale session in the picker" |
Focus after error() closes a conversation |
Fixed, with Dewey's check on prior focus. D05 and D06 both die |
On the error() deviation: I agree with Dewey. My note asked for the
fallback without the check, and that would pull focus to <main> and
scroll on every failed ten-second refresh when nothing had focus. The
check is better than what I asked for.
B2 also covers the mirror selects, which keep() and restore() now
carry: focus on each Settings mirror (palette, mode) survives the 10 s
refresh, and a change on the mirror moves the command bar's select.
Required
-
Arbiters always reads "none".
reads.mjs:149passesb.arbitersthrough as the business file has it, an object ({ delivery, technical }, whichpackages/busrequires).bus.js:404renders it withnames(), andnames()(bus.js:348) returnsnonefor anything but a non-empty array. B1 on the seeded fixture: the API answers{"delivery":"pm","technical":"cto"}and the page shows "none". The views.test stub hasarbiters: { delivery: 'pm' }and nothing asserts the Arbiters line, so no test sees it. Render the object as pairs (for example "pm (delivery), cto (technical)"), and assert the line on the seeded fixture, not only on the stub. -
An import failure in the queue child sends an absolute path to the browser.
queue-read.mjsimportsstore.mjsanderrors.mjsstatically, so a failure at load time happens before itstry. Node prints the error and a stack to stderr and exits 1.reads.mjs:81passes up to 2000 characters of that stderr as theread-failedmessage, and the redactor doesn't match a path afterfile://(the character before the/is:, which isn't in its prefix set). P4f, with the realqueue-read.mjs:store.mjswith a syntax error: the message startsfile://<BASE>/syntax/packages/queue/src/store.mjs:1, then the broken source line, then the stack.- no
store.mjs: the message ends withurl: 'file://<BASE>/missing/packages/queue/src/store.mjs'.
<BASE>is my substitution in the probe output; the real message carries the absolute temporary path. This checkout is edited in place by several seats, so a half-writtenstore.mjsis a real state, not a contrived one. The brief says no file path appears in any response. Any one of these closes it, and I'd do the first two:- import the store inside the
try(a dynamicimport()), so a load failure prints "the queue read failed"; - forward stderr only for exit 2, with a fixed message for any other exit;
- add
:(orfile:) to the redactor's prefix set.
A test with a broken
store.mjsin thequeueReposcratch copy should assert that the message carries no base path.
Notes (not blocking)
- Redactor gaps from P2. These pass through unchanged:
config:/home/u/x.token,path</home/u/x>,~/secret/x.token,./secret/x.token, a Windows path,id_123456789012345678, and digit runs of 16 or 21. None of the sources I traced produces the first two today.~/paths do show up in real queue notes (~/.mosaicin three), and the Queue row page shows them as written. They name no user and no secret, so I'd leave them. The README's "any other absolute path" describes this correctly. - Five mutants survive (table below). D14 is equivalent. D20 to D22 are paths my probes show working with no test. D29 is a test gap for mirror focus across a refresh. I'd add the D29 test with the round 2 fixes; the others can wait.
cli.mjs's outer catch prints its error unredacted. Dewey lists it under "Not done"; I agree its inputs carry no path today.bus.js:428lost a space:const GROUPS =[[. Cosmetic.- B4 is weaker than Dewey's test: after a navigation one refresh is due, and that is the new view's timer. Dewey's PROBE checks the count with the reads held, which is the right shape. D01 confirms it.
- views.test test 1 failed at line 167 (the palette's queue rows) in two mutant runs that can't reach it, and passed in eight clean runs. Worth a look before it fails someone's gate. Two guesses, neither checked: the Ctrl+K press lands before the palette's handler sees the last queue read, or a real 10 s refresh fires during the test (it runs about 5 s, but slower under load).
Probes
| Probe | Candidate |
|---|---|
| P2 redactor | quoted, bracketed, braced and tab-led paths become <path>; the config dir is <config>; .mosaic-dev.bak is <path>; Discord URL ids and id: ids are <id>. Gaps in note 1 |
| P3 routes | on all four: bad Host 403, bad Origin 403, HEAD and OPTIONS 405. /api/queue/ 400, /api/queue%2F1 404, /api/queue/1?x=1 reaches the read |
| P4a child hangs, 500 ms timeout | read-failed "did not finish in time" at 500 ms |
| P4b 64 KiB out, 1 KiB cap | invalid-response "printed too much" |
| P4c exit 2 with a path | queue-refused "refused at <path>" |
| P4d wrong shape | invalid-response |
| P4e killed by a signal | read-failed "did not finish" |
| P4f import failure | absolute file:// path and stack in the message. Required 2 |
| P5 bus host state | no file and a stale file give no-bus-host; a live file gives its business; a malformed file gives no-bus-host "unreadable" |
| B1 Business, seeded | API {"delivery":"pm","technical":"cto"}, Arbiters "none". Required 1 |
| B2 mirror focus across the refresh | kept on palette and on mode; a mirror change moves the command bar |
| B3 H1 focus across the refresh | kept |
| B4 navigation | one refresh due after it. See note 5 |
Mutants
26 of 31 killed (r1/mut/summary.txt). I checked each kill site in its
output against its diff.
| Mutant | Change | Result |
|---|---|---|
| D01 | no clearTimeout(timer) at the top of render() |
killed, "the navigation cleared the due refresh", views.test.mjs:150 |
| D02 | keep() doesn't record an H1 |
killed, "#/queue keeps heading focus", views.test.mjs:95 |
| D03 | restore() doesn't refocus the H1 |
killed, same line |
| D04 | no #conv-pick clear |
killed, "no stale session in the picker", shell.test.mjs:220 |
| D05 | no focus fallback in error() |
killed, "focus after a refusal closes the conversation", shell.test.mjs:231 |
| D06 | the fallback without focused && |
killed, "a failure with nothing focused leaves focus alone", shell.test.mjs:191 |
| D07 | no generic path rule in the redactor | killed, redactor test reads.test.mjs:23 and "response carries /srv/secret" |
| D08 | no Discord id rule | killed, reads.test.mjs:25 and "response carries id 523456789012345678" |
| D09 | no unquote | killed, reads.test.mjs:27 |
| D10 | no config and dataRoot labels | killed, reads.test.mjs:21, :110 and :155 |
| D11 | queue list not scrubbed | killed, reads.test.mjs:49 and "response carries id" in views.test |
| D12 | queue row not scrubbed | killed, the row note text, views.test.mjs:194 |
| D13 | business vars not allowlisted | killed, "response carries discordUserId" |
| D14 | role vars not allowlisted | survived, equivalent today. See below |
| D15 | credential spreads its ref | killed, "response carries CODER_GITEA_SECRET_ENV" |
| D16 | fail() doesn't redact |
killed, reads.test.mjs:110 and views.test.mjs:208 |
| D17 | settings not scrubbed | killed, reads.test.mjs:155 |
| D18 | cli bus line not redacted | killed, "the log names the temporary directory", views.test.mjs:232 |
| D19 | refusal on exit 3, not 2 | killed, read-failed for queue-refused, reads.test.mjs:72 |
| D20 | no output cap | survived |
| D21 | timeout times 1000 | survived |
| D22 | host state without .live |
survived |
| D23 | no 405 on the new routes | killed, reads.test.mjs:61 |
| D24 | any digits as a row id | killed, 404 for 400, reads.test.mjs:59 |
| D25 | no 503 without reads | killed, reads.test.mjs:136 |
| D26 | queue-refused not a refusal |
killed, no refusal banner, views.test.mjs:107 |
| D27 | settings refusal without appearance | killed, 0 mirror selects for 2, views.test.mjs:114 |
| D28 | palette without queue rows | killed, views.test.mjs:167 |
| D29 | keep() without the mirror |
survived. See below |
| D30 | mirror change ignored | killed, #mode never "dark", views.test.mjs:177 |
| D31 | rows() drops its notes |
killed, write.test.mjs:349 |
D14 is equivalent. The business var registry (packages/business/src/vars.mjs)
allows only harness, model, thinking and the three limits.* keys at
the agent layer, which is the allowlist. It stays as defense in depth.
D20, D21 and D22 are untested paths that work. P4a and P4b show the timeout and the cap behave, and P5c shows a live host file is read. A test for each would be short, with the reader pointed at a fake script.
D29 is a real gap. With the mutant applied, B2 shows focus on either
mirror select falls to <body> on the 10 s refresh
(r1/probes/probe-browser-D29.txt). B2 passes on the candidate, so the
code is right and only the test is missing.
One more thing showed up in the mutant outputs. views.test test 1 timed
out at line 167, where Ctrl+K should list #/queue/7, under D06, D07 and
D09 as well as D28. D07 and D09 change only the redactor, and test 1's
stub reads never call it, so those two failures aren't the mutants'
doing. D06, D07 and D09 each die at their own assertions as well. Eight clean
runs of the webui suite on the candidate passed (r1/flake/summary.txt),
so it's intermittent and I don't know the cause. See note 6.
Files
r1/candidate-manifest.sha256: copy of Dewey's.r1/gate.sh,r1/out/: suite runs,summary.txtandtest-release-docker.txt.r1/mut/: mutant definitions, runner, diffs, outputs,summary.txtand the manifest check after the runs.r1/probes/: the three probes and their output, plus B2 under D29.r1/flake/: eight clean runs of the webui suite.