Files
stack/agents/darkwing/work/queue-54-review/review-r1.md
T
jason.woltjeandClaude Opus 5.5 8a3ded5335 review(row54): round 1 packet, changes (darkwing)
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]>
2026-10-10 15:47:54 -05:00

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 papply and mutwt at c6b214e5 with row54.patch applied and staged. Both check 14 OK. mutwt still checks 14 OK after the mutant runs (r1/mut/manifest-after.txt).
  • r1/gate.sh: the packages/webui, packages/queue and packages/business node suites, build-tokens.mjs --check, then every scripts/test-*.sh with DOCKER_HOST=unix:///nonexistent.sock, 20:05:27Z to 20:08:51Z. I skipped the packages/cli node suite: packages/cli doesn't change and is frozen.
  • 31 mutants (r1/mut/mutate.py, r1/mut/run.sh), each against the full packages/webui suite, except D31 against packages/queue, 20:09:04Z to 20:37:00Z.
  • test-release with Docker.
  • Node probes (r1/probes/probe-node.mjs, output probe-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, output probe-import.txt): the real queue-read.mjs beside a store.mjs with a syntax error, and beside none (P4f).
  • Browser probes in Chromium through the candidate's tests/browser.mjs on the seeded fixture with the real reads (r1/probes/probe-browser.test.mjs, output probe-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

  1. Arbiters always reads "none". reads.mjs:149 passes b.arbiters through as the business file has it, an object ({ delivery, technical }, which packages/bus requires). bus.js:404 renders it with names(), and names() (bus.js:348) returns none for 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 has arbiters: { 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.

  2. An import failure in the queue child sends an absolute path to the browser. queue-read.mjs imports store.mjs and errors.mjs statically, so a failure at load time happens before its try. Node prints the error and a stack to stderr and exits 1. reads.mjs:81 passes up to 2000 characters of that stderr as the read-failed message, and the redactor doesn't match a path after file:// (the character before the / is :, which isn't in its prefix set). P4f, with the real queue-read.mjs:

    • store.mjs with a syntax error: the message starts file://<BASE>/syntax/packages/queue/src/store.mjs:1, then the broken source line, then the stack.
    • no store.mjs: the message ends with url: '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-written store.mjs is 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 dynamic import()), so a load failure prints "the queue read failed";
    • forward stderr only for exit 2, with a fixed message for any other exit;
    • add : (or file:) to the redactor's prefix set.

    A test with a broken store.mjs in the queueRepo scratch copy should assert that the message carries no base path.

Notes (not blocking)

  1. 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 (~/.mosaic in 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.
  2. 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.
  3. cli.mjs's outer catch prints its error unredacted. Dewey lists it under "Not done"; I agree its inputs carry no path today.
  4. bus.js:428 lost a space: const GROUPS =[[. Cosmetic.
  5. 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.
  6. 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.txt and test-release-docker.txt.
  • r1/mut/: mutant definitions, runner, diffs, outputs, summary.txt and 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.