394 lines
25 KiB
Markdown
394 lines
25 KiB
Markdown
# Row 54 (#1543): Read-only console views: Queue, Business and Settings
|
||
|
||
Dewey, 2026-10-10. Brief: `docs/plans/2026-10-10_design-implementation.md`,
|
||
section "Read-only console views: Queue, Business and Settings" (blob
|
||
a360554c), with `docs/design/IMPLEMENTING.md` "Strings", "Boundaries" and
|
||
"Acceptance" as for row 53. Lead decisions 81 (appearance follows the
|
||
system by default) and 83 (52c5a1f8: `rows()` in packages/queue, path
|
||
redaction, recorded choices stand). The brief's "No change to
|
||
`packages/queue`" is superseded by decision 83 for exactly three paths;
|
||
the brief file was left alone because rows 53-61 pin its blob.
|
||
|
||
## Base
|
||
|
||
Row 54 is `after: 53 done`. Row 53 landed at d64f434f (feat 1bdb6f9b,
|
||
#1542 closed in comment 27175), and this packet is built and gated on
|
||
d64f434f. It was first built on row 53's round 2 candidate (row 54
|
||
manifest `8258420b`), then rebased onto round 3 (`3347bb61`); round 3's
|
||
`round3.patch` applied cleanly. The rebase onto d64f434f changed nothing in
|
||
row 54's files: `row54.patch` as built on round 3, applied at d64f434f,
|
||
reproduced `3347bb61` (14 OK). Filbert's T8 test was then added (see
|
||
"Tests added from review").
|
||
|
||
Row 54's candidate is 14 files, listed in `candidate-files.txt`, with
|
||
sha256 in `candidate-manifest.sha256` (manifest sha256
|
||
`dba2429e411ae22b1e939344247e9f3857a48e488dc7da11c578d1092288ea8a`).
|
||
`row54.patch` is the delta over d64f434f: applied to a clean d64f434f
|
||
export, it reproduces the manifest (14 OK, checked). `index.html` is
|
||
unchanged; the views live in `bus.js`, and `app.js` changes only for two
|
||
of the fixes to HEAD behaviour.
|
||
|
||
## What changed
|
||
|
||
- `packages/queue/src/store.mjs` (decision 83): `export function rows(opts)`,
|
||
`{ rows: rowsArray(state), err: notes, code: 0 }`, the same read as `list`
|
||
with no write. `packages/queue/README.md` names it;
|
||
`packages/queue/tests/write.test.mjs` adds one test: the rows match `list`
|
||
and `show` row for row, no queue, view or head file changes, the
|
||
reader-between notes match `list`'s, and a disagreement refuses with
|
||
exit code 2. Nothing else in packages/queue changes.
|
||
- `packages/webui/src/queue-read.mjs` (new): a child process that calls
|
||
`rows()` with its cwd at the checkout and prints JSON. A refusal prints
|
||
the store's message on stderr and exits with the store's code.
|
||
- `packages/webui/src/reads.mjs` (new): `consoleReads` returns `queue`,
|
||
`row`, `business` and `settings`. The queue read runs the child with a
|
||
timeout and an output cap. Exit 2 is `queue-refused` (fail closed). Any
|
||
other failure is `read-failed`, and the view keeps its last read. Business
|
||
is `loadBusiness` projected to names, vars on an allowlist,
|
||
`resolveInstance`/`classify` authority per action, and credential metadata
|
||
(service, account, role, date kind, date, state from the date alone).
|
||
Settings is `RELEASE`, the business id and `readNotifyConfig`'s binding
|
||
name. `redactor` maps the config dir to `<config>`, dataRoot to
|
||
`<dataRoot>`, any other absolute path to `<path>`, a 17 to 20 digit run
|
||
to `<id>`, and drops V8's JSON parse quote. `scrub` applies it to every
|
||
string in a body.
|
||
- `packages/webui/src/serve.mjs`: GET `/api/queue`, `/api/queue/<id>`,
|
||
`/api/business`, `/api/settings`. Any other method gets 405, and a bad
|
||
row id gets 400 `invalid-request`. Statuses: 503 for a refusal or nothing
|
||
to read, 404 not-found, 502 an unusable answer. A non-`ReadError` answers
|
||
with its code only. Settings answers 200 without a system config, with
|
||
`notifier.refused`. The existing CSP, Host and Origin checks are
|
||
unchanged and cover the new paths.
|
||
- `packages/webui/src/cli.mjs`: builds `consoleReads` and runs the
|
||
"Bus: not configured (…)" startup line through the redactor.
|
||
- `packages/webui/src/public/bus.js`: views `#/queue` (state filter),
|
||
`#/queue/<id>`, `#/business`, `#/settings`; three section links; queue
|
||
rows in the Ctrl+K palette; `queue-refused` is a refusal; the command bar
|
||
names the source; a Settings refusal keeps appearance (its selects mirror
|
||
the command bar's). It also carries two of the fixes to HEAD behaviour
|
||
below.
|
||
- `packages/webui/src/public/app.js`: the other two fixes to HEAD behaviour,
|
||
three lines. Nothing for the views.
|
||
- `packages/webui/README.md`: the row 54 section, data and boundaries,
|
||
the verify commands, the two `app.js` fixes, and the failed first read
|
||
in the shell tests line.
|
||
- Tests: `reads-fixture.mjs` (seeded fixture), `reads.test.mjs` (8),
|
||
`views.test.mjs` (3), `shell.test.mjs` (seven sections in the list and
|
||
palette, the two `app.js` fixes, and Filbert's T8: a failed first read).
|
||
|
||
## Acceptance against the brief
|
||
|
||
| Brief item | Where it is shown |
|
||
|---|---|
|
||
| GET JSON `/api/queue`, `/api/queue/<id>`, `/api/business`, `/api/settings` | reads.test "queue: rows, one row, ids and methods, notes"; serve routes |
|
||
| CSP, Host and Origin checks kept | serve.mjs unchanged around the new paths; serve.test and bus-routes tests green |
|
||
| No write, no network, no new dependency | views.test PROBE: every request is GET; `rows()` test: queue, view and head files byte-identical; no package.json change |
|
||
| Five states on each view | views.test test 1: loading, normal, stale over kept rows, refusal, empty on `#/queue`, `#/queue/6`, `#/business`, `#/settings` |
|
||
| Missing or invalid business file: refusal with the module's text | views.test test 2: "business file not found: `<config>/businesses/acme.json`" and "business file is not valid JSON (`<config>/…`)"; reads.test "business: missing or invalid file" |
|
||
| Secrets as metadata only | reads.test "business: … credential metadata only"; views.test test 2 runs `clean()` on every page and every response body |
|
||
| No value, token or path in any response or log | `clean()` asserts no tmp base, no `/srv/secret`, no `"file"`, `"env"`, `"tokenFile"`, `CODER_GITEA_SECRET_ENV`, `placeholder-not-a-token`; views.test test 3 asserts the startup log names no config path and no secret |
|
||
| Notifier: binding names only, no channel, guild or user id | seeded fixture holds five Discord ids in the binding, the business vars and a queue note; `clean()` asserts none appears; the note shows as `token at <path> for <id>` |
|
||
| Settings: appearance, notifications, about | views.test test 1: the settings mirror sets mode dark and localStorage records it; settings dl shows binding name, release 0.0.99, loopback address |
|
||
| Queue rows: state, owner, reviewers, brief link | views.test test 1 checks the table cells as text |
|
||
| Move only through `queue move`; command, not a button | views.test test 1: the row page's `.s1-cmd code` is `scripts/mosaic queue move 6 <state> --op <op> --by <seat>`; no control on any of the four views but Copy and Read again |
|
||
| Tests as in row 53 | 400px viewport, no sideways scroll (`flat`), focus to the H1 on navigation and kept on refresh, hostile text inert (row 6's piece holds markup), palette rows as text, no script errors |
|
||
|
||
## Tests
|
||
|
||
Full webui suite 38 pass (row 53: 27, plus 8 in reads.test and 3 in
|
||
views.test; the T8 and fix assertions sit inside existing tests). packages/queue node suite 149 pass, test-queue 27 passed.
|
||
|
||
## Mutations
|
||
|
||
Each mutant was applied to a scratch copy and run against its test
|
||
(`~/dewey-scratch/r54/mutants.py`, outputs in `~/dewey-scratch/r54/out/`).
|
||
17 of 17 killed (rerun 2026-10-10T19:54Z on d64f434f, with all four
|
||
fixes to HEAD behaviour and the T8 test in the tree;
|
||
`out/mutants-run3.txt`).
|
||
|
||
| Mutant | Killed by |
|
||
|---|---|
|
||
| startup log without the redactor | "the log names the temporary directory" |
|
||
| `queue-refused` not a refusal | no refusal banner (timed out) |
|
||
| a Move button on the row page | "#/queue/6: no control but Copy and Read again" |
|
||
| credential spreads its ref (file, env) | "response carries CODER_GITEA_SECRET_ENV" |
|
||
| Settings refusal drops appearance | "#/settings": 0 mirror selects, expected 2 |
|
||
| module refusal keeps data | "#/queue: no kept queue row in the palette after not-configured" (2, expected 0) |
|
||
| palette queue rows unescaped | row 6's `<b id="q-b">` appears in the palette (1, expected 0) |
|
||
| no Discord id redaction | "response carries id 523456789012345678" |
|
||
| no generic path redaction | "response carries /srv/secret" |
|
||
| no H1 focus kept on refresh | "#/queue keeps heading focus" |
|
||
| no `clearTimeout(timer)` at the top of `render()` | "the navigation cleared the due refresh" |
|
||
| no `#conv-pick` clear when a conversation opens | "no stale session in the picker" |
|
||
| no focus fallback at the end of `error()` | "focus after a refusal closes the conversation" |
|
||
| the fallback without the "had focus" check | "a failure with nothing focused leaves focus alone" |
|
||
| empty board only while the refusal state is up (Filbert's `nodata-after-refusal-only`) | "no skeleton after a failed first read"; survives without T8, checked on the pre-T8 tree |
|
||
| `rows()` drops its notes | `rows:` test, reader-between notes |
|
||
| queue child exits 1 on a refusal | "a refused read fails closed": `read-failed`, expected `queue-refused` |
|
||
|
||
## Gate
|
||
|
||
Worktree at d64f434f (row 53 landed), then row 54's 14 files
|
||
(`sha256sum -c` of this manifest: 14 OK). node_modules linked from the
|
||
checkout, TMPDIR in scratch, Docker available, suites run one after
|
||
another, 2026-10-10T19:54:26Z to 19:59:36Z. `core.hooksPath` unset.
|
||
Script `~/dewey-scratch/r54/gate.sh`, outputs `~/dewey-scratch/r54/gate/out/`.
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| `node --test 'packages/webui/tests/*.test.mjs'` | 38 pass, 0 fail |
|
||
| node suites of business, bus, cli, control-board, conversation, discord, ledger, mosaic, queue, runs, seat, tasks | 60, 67, 66, 124, 182, 178, 78, 69, 149, 41, 19, 51 pass; 0 fail in each |
|
||
| `node docs/design/tools/build-tokens.mjs --check` | rc 0, "tokens.css is current" |
|
||
| test-auth | 15 passed, 0 failed |
|
||
| test-conductor | 17 passed, 0 failed |
|
||
| test-config | 24 passed, 0 failed |
|
||
| test-discord | 66 passed, 0 failed |
|
||
| test-extension-package | 18 passed, 0 failed |
|
||
| test-foundation | 44 passed, 0 failed |
|
||
| test-queue (decision 83) | 27 passed, 0 failed (verify and render --check skip outside the canonical root) |
|
||
| test-release | 14 passed, 0 failed |
|
||
| test-task | 98 passed, 0 failed |
|
||
|
||
The secret, id and path assertions are in the webui count: reads.test
|
||
(8) and views.test (3). packages/queue is 149 against row 53's 148: the
|
||
one `rows()` test.
|
||
|
||
## Fixes to HEAD behaviour (not row 54 features)
|
||
|
||
Four defects, none a row 54 feature, all in HEAD at d64f434f. The two in
|
||
`bus.js` were there before row 53. Of the two in `app.js` (Darkwing's
|
||
non-blocking notes on row 53 round 3, #1542 comment 27171), the picker
|
||
was there before row 53 and the focus loss came with row 53 round 3. Sage put all four in this row, each with its own test
|
||
(2026-10-10).
|
||
|
||
### Refresh timer during a navigation
|
||
|
||
Darkwing's row 53 round 2 note 3 (#1542 comment 27165), moved here by
|
||
Sage. `render()` set the 10 s refresh timer after each read but never
|
||
cleared it when a new render started. If the timer fired while a
|
||
navigation's read was pending, the refresh bumped the render generation,
|
||
and the navigation's render returned early without focusing its H1, so
|
||
focus fell to `<body>`. The fix is `clearTimeout(timer)` at the top of
|
||
`render()`.
|
||
|
||
views.test test 1 pins it without a real clock. The PROBE wraps
|
||
`setTimeout` and `clearTimeout` for the 10 s `render(false)` timer only
|
||
(app.js's 10 s board timer is left alone) and exposes `fireRefresh()`. The
|
||
test checks that one refresh is due on `#/queue`, holds the reads, and
|
||
navigates to `#/business`. It then asserts that `fireRefresh()` finds no
|
||
refresh due, releases the reads, and asserts that the Business H1 has
|
||
focus. The "no timer clear" mutant is killed at "the navigation cleared the
|
||
due refresh". With that count assertion removed, the mutant still fails
|
||
at "the navigation keeps heading focus", so the race reproduces every run.
|
||
|
||
### Heading focus on a same-page refresh
|
||
|
||
A refresh
|
||
of the same page rebuilds the view, and `restore()` put focus back only on
|
||
a link, Copy, Retry or a mirror select. If the H1 held focus, as it does
|
||
right after navigation, focus fell to `<body>`. With the 10 s refresh this
|
||
can fail bus-browser's "activeElement is H1" check under load. One full
|
||
webui run here failed it at 40 s, and three runs of that file alone passed.
|
||
Sage's row 53 round 2 gate didn't hit it. The fix: `keep()` records an H1
|
||
with focus, and `restore()` focuses the new H1. views.test pins it, and the
|
||
"no H1 focus kept on refresh" mutant confirms the test.
|
||
|
||
### Session picker after a failed catalogue read (`app.js`)
|
||
|
||
`openConversation()` replaced the `#conv-pick` options only after a good
|
||
catalogue read. If that read failed, the picker kept the last seat's or
|
||
the last read's sessions under "History unavailable". The fix empties the
|
||
picker where the view already empties `#conv-meta` and `#conv-log`. It is
|
||
not disabled there: a change of session starts from the picker, and
|
||
disabling a focused control drops focus to `<body>`.
|
||
|
||
The shell browser test opens History (one session in the picker), makes
|
||
the board answer 500, opens History again, waits for "History
|
||
unavailable", and asserts the picker has no option. The "no picker clear"
|
||
mutant is killed at "no stale session in the picker".
|
||
|
||
### Focus after a refusal closes a conversation (`app.js`)
|
||
|
||
Row 53 round 3's `error()` closes an open conversation on a refusal.
|
||
`closeConversation()` gives focus back to the History button, and the
|
||
empty board then removes that button, so focus fell to `<body>`. The fix:
|
||
`error()` notes whether anything had focus when it started, and if it
|
||
ends with focus on `<body>`, focuses `<main>`, the same fallback
|
||
`closeConversation()` uses. Darkwing proposed the fallback without the
|
||
first check. With nothing focused, that would move focus and scroll to
|
||
`<main>` on every failed read, including a failed first load and each
|
||
failing ten-second refresh.
|
||
|
||
The shell browser test asserts that after the refusal closes the
|
||
conversation, focus is on `#main`. It also blurs before the 503 that
|
||
follows the first refusal and asserts focus stays on `<body>`. Mutants:
|
||
"no focus fallback" is killed at "focus after a refusal closes the
|
||
conversation", and "fallback without the check" at "a failure with
|
||
nothing focused leaves focus alone".
|
||
|
||
## Tests added from review
|
||
|
||
Filbert's row 53 round 3 verdict (#1542 comment 27174) named two
|
||
non-blocking items. Sage put T8 in this row (2026-10-10).
|
||
|
||
- T8: round 3 draws the empty board after a failed first read, but no test
|
||
asserted it, so `nodata-after-refusal-only` survived. Test only, no code
|
||
change. The shell browser test reloads the page with the board answering
|
||
503 and asserts the error banner ("No board data loaded."), no
|
||
`aria-busy` skeleton, empty `#sessions` and `#waiting`, the three counts
|
||
`–`, and "the read failed" in `#status`, the footer, `#fresh-board` and
|
||
the tree. It then answers 200 and refreshes back to the empty board. The
|
||
reload resets `window.errors`, so the test asserts it is empty before
|
||
reloading.
|
||
- N8 is Darkwing's note 2, fixed above under "Focus after a refusal closes
|
||
a conversation".
|
||
|
||
## Decisions and deviations
|
||
|
||
- A refusal from any source (`queue-refused`, `not-configured`,
|
||
`no-bus-host`, 403) clears every kept read, as row 53 round 2 does for the
|
||
bus, so no kept queue row survives a refusal in the palette.
|
||
- The brief link shows as the path and anchor, not a link to the file:
|
||
Console serves no repository files.
|
||
- The queue read runs as a child process, like the bus read, so a slow read
|
||
never holds up the HTTP server. Like `list`, it takes the queue lock only
|
||
to recheck a disagreement before reporting it. It writes nothing.
|
||
- The business shown is `--business`, else the running bus host's. Without a
|
||
system config, Business is `not-configured` and Settings answers with
|
||
`notifier.refused`.
|
||
- `app.js` carries no view code. The brief expected both rows to change it
|
||
for the views, but they fit the bus view's router in `bus.js`. Its only
|
||
change is the two fixes to HEAD behaviour.
|
||
|
||
## Not done here
|
||
|
||
- `cli.mjs` prints an error from its outer catch unredacted
|
||
(`refused: …`). That catch only reaches option, board-origin and bind
|
||
errors, none of which carry a path, so it is left as it was.
|
||
- Out of scope per the brief: Ledger, runs and releases, bus backend reads.
|
||
|
||
## Boundaries kept
|
||
|
||
No change under `packages/business`, `packages/cli`, `packages/bus`,
|
||
`packages/tasks`, `scripts/` or `public/shared/`. packages/queue changes only
|
||
in decision 83's three paths. No new dependency. No live tracker request,
|
||
no Gitea write other than this row's posts, no push.
|
||
|
||
## Round 2 (answers #1543 comments 27185 and 27186)
|
||
|
||
Filbert (comment 27185, rev 376) and Darkwing (comment 27186, rev 377)
|
||
both asked for changes. Sage combined them into one round 2 list: B1, R1,
|
||
Arbiters and the queue-read import leak are required; D29 and the
|
||
views.test :167 timeout are optional, with a finding reported either way.
|
||
Same base, d64f434f. Candidate manifest `candidate-manifest.sha256`
|
||
(sha256 afb2ae0e…) covers the same 14 files. `row54.patch` is against
|
||
d64f434f; applied to a `git archive` of d64f434f it reproduces all 14
|
||
files (14 OK). `row54-r1-to-r2.patch` is the change from round 1 (7
|
||
files). Round 1's packet is kept as `candidate-manifest-r1.sha256` and
|
||
`row54-r1.patch`.
|
||
|
||
| Item | Fix | Test |
|
||
|---|---|---|
|
||
| B1 (Filbert, blocker) | `afterRef` renders an `{id, when}` entry as a link to the row and its condition, "6 settled", as QUEUE.md writes it | Seeded views test: `#/queue/9` After is `<a href="#/queue/6">6</a> settled`; row 11 carries `{id: 9, when: 'done'}`, the shape of real row 54's `{id: 53, when: 'done'}`, and reads `<a href="#/queue/9">9</a> done`. Both go through the real store. Reads test: `/api/queue/11` keeps `after` as stored |
|
||
| R1 (Filbert), with Sage's `file:/x` note | The path rule also matches `~/`, and a path after `:` or `<`. A `:` followed by `//` and a host is a URL and keeps its path; `file:///x` is still a path | Redactor test: row 35's own phrase, row 8's `` `~/.mosaic` ``, `key=~/k`, `file:/x`, `file:///srv/y`, `</srv/z.token>`; three URLs unchanged; `a~/b`, a bare `~` and a relative path unchanged. The seeded note is shaped like row 35's and is asserted redacted in the reads and views tests; `clean()` now also refuses `~/` and `secrets/mosaic-stack` in any body or page |
|
||
| Arbiters (Darkwing, required) | A business file's `arbiters` is an object, `{delivery: 'pm', technical: 'cto'}`; `names()` took only arrays, so the view always said "none". `arbiterRefs` renders each pair as "instance (kind)"; an array still goes through `names()` | Seeded views test, through `loadBusiness`: Arbiters reads `pm (delivery), cto (technical)`. Stub views test: `pm (delivery)` |
|
||
| Import leak (Darkwing, required) | Both, since both were cheap. `queue-read.mjs` imports the store and its error class with `await import()` inside its `try`, so a store that fails to load prints "the queue read failed" and exits 1. The reader forwards the child's stderr only on exit 2 (the store's refusal, `queue-refused`); any other exit is the fixed `read-failed` "the queue read failed (exit N)" | Reads test, in a `queueRepo` copy: store.mjs with a syntax error, then store.mjs missing. Each asserts the child's exact status, stdout and stderr (`1`, empty, `the queue read failed\n`), then `/api/queue` gives 503 with that fixed body and `clean()` finds no base or repo path. A fake child that prints a `file:///srv/q/x.mjs` stack and exits 3 gives the fixed exit-3 message |
|
||
| D29 (Darkwing, optional) | none needed: the code keeps the mirror select's focus | Five-state views test: focus the mirror select on `#/settings`, mark the H1, fire the 10 s refresh, wait for the redraw, assert focus is on the mirror select |
|
||
| D20, D21 (Darkwing, optional) | none needed | Reads test, with the reader pointed at a fake child: one that never exits, with `timeoutMs: 300`, gives "the queue read did not finish in time"; one that prints 64 KiB, with `maxBytes: 1024`, gives "the queue read printed too much" |
|
||
| :167 timeout (Sage item 5) | Test only. Cause below | Five-state views test |
|
||
| N1 (Filbert) | `settings()` refuses `not-configured` in Console's words when there is no data root, before `readNotifyConfig` | Reads settings test: `--business acme` with no system config gives that notifier refusal, not Node's TypeError |
|
||
| N2 (Filbert) | Required reads "yes", with "since …" only for a real date | Seeded views test on `#/queue/9` and `#/queue/11` |
|
||
| N3 (Filbert), Darkwing note 4 | `const GROUPS = [[` | none needed |
|
||
| T1 (Filbert) | none needed: the scrub was already there | Fixture: the reviewer's `model` var (the one allowlisted free-text var) holds `/srv/secret/models/local.gguf`; the business test asserts `{ model: '<path>' }` |
|
||
|
||
### The :167 timeout
|
||
|
||
The five-state test's server had reads but no bus. Opening the palette
|
||
reads `/api/bus/inbox` and `/api/bus/tasks`, and with no bus those
|
||
answer 503 `not-configured`. That is a refusal, so the row 53 rule,
|
||
"a refusal forgets everything", cleared the last reads, `api:queue`
|
||
included, and the palette rebuilt without the queue rows. The check
|
||
`palette includes #/queue/7` passed only when its first poll ran before
|
||
those two reads returned. Under load they returned first and the wait
|
||
timed out. It was a race, not a timeout set tighter than the work it
|
||
waits on, so a wider timeout would not have fixed it.
|
||
|
||
The fix is in the test. Its server now has a stub bus that answers
|
||
empty lists, and the check first waits for the palette's own inbox and
|
||
tasks reads to answer, then asserts the queue rows are still listed. The
|
||
`views-no-bus` mutant drops the stub bus. It is killed at that check on
|
||
every run, which confirms the cause.
|
||
|
||
Product follow-up, not changed in this row: on a Console with reads and
|
||
no bus, each palette open forgets the queue rows. That follows the row 53
|
||
refusal rule as written. Whether `not-configured` on a bus route should
|
||
forget the reads is a question for row 53's owner and for Sage.
|
||
|
||
### A second flake, found while testing D29
|
||
|
||
The new D29 check failed 3 of 8 times under mutant load: focus was on the
|
||
H1, not the mirror select. An instrumented run traced the late focus to
|
||
the palette dialog's `close` handler (`bus.js`, `cmdk-dialog`'s `close`
|
||
listener calls `pkBack.focus()`). Esc closes the dialog at once, but
|
||
the `close` event is a later task, and under load it ran after the test
|
||
had focused the mirror select. The test's `closePalette()` now waits for
|
||
that event before going on. Both Esc sites use it. After the change: 16
|
||
of 16 parallel runs of the five-state test passed, and the webui suite
|
||
was 39/0 while the mutant set ran beside it.
|
||
|
||
### Not done, as follow-ups
|
||
|
||
- D22, host state without `.live`: no test. It needs a live bus host's
|
||
state file in a fixture; Darkwing's P5c shows the read works.
|
||
- `cli.mjs`'s outer catch prints its error unredacted. Its inputs carry no
|
||
path today (Darkwing note 3 agrees).
|
||
- Darkwing's P2 redactor gaps, run through the round 2 redactor:
|
||
`~/secret/x.token` is `<path>`, `config:/home/u/x.token` is
|
||
`config:<path>`, and `path</home/u/x>` is `path<<path>>`, since `<` is
|
||
now a path prefix. Still unchanged:
|
||
`./secret/x.token`, a Windows path, `id_123456789012345678`, and digit
|
||
runs of 16 or 21. The README states the rule.
|
||
|
||
The queue store refuses `<` in a note, so a note can't carry the `<…>` form;
|
||
that case is in the redactor unit test only. Known gap, stated in the
|
||
README: a path stops at the first space, so a path containing a space is
|
||
redacted only up to that space. A bare `~`, `~user/` and relative paths
|
||
are not paths to the redactor.
|
||
|
||
### Mutations, round 2
|
||
|
||
`mutants.py`, 33 of 33 killed, each site matched once
|
||
(`out/mutants-r2-full.txt` in scratch), run after the `closePalette()`
|
||
change. The 17 from round 1 still apply (the `no-path-redaction` site
|
||
updated to the new rule), and 16 are new:
|
||
|
||
| Mutant | Change | Killed by |
|
||
|---|---|---|
|
||
| qlink-all-text | After maps through `qLink` again (round 1's code) | seeded views, `#/queue/9` After |
|
||
| after-no-when | the condition dropped | seeded views |
|
||
| required-bare-true | Required shows the boolean again | seeded views |
|
||
| path-no-tilde | `~/` not a path | redactor test |
|
||
| path-no-colon | `:` not a path prefix | redactor test |
|
||
| path-no-lt | `<` not a path prefix | redactor test |
|
||
| path-eats-urls | the URL guard dropped | redactor test |
|
||
| business-noscrub | `/api/business` not scrubbed (Filbert's) | business test, `/srv/secret` |
|
||
| settings-null-dataroot | the N1 refusal dropped | settings test |
|
||
| arbiters-names | Arbiters through `names()` again (Darkwing's) | seeded views, Arbiters |
|
||
| static-store-import | static imports of the store and its error class | reads test, broken store, child's stderr |
|
||
| forward-any-exit | any exit forwards the child's stderr | reads test, broken store |
|
||
| no-mirror-keep | `keep()` without the mirror (Darkwing's D29) | five-state views, mirror focus after the refresh |
|
||
| no-output-cap | the output cap dropped (Darkwing's D20) | reads test, fake child, 64 KiB |
|
||
| timeout-x1000 | the timeout times 1000 (Darkwing's D21) | reads test, fake child, by the test's 60 s timeout |
|
||
| views-no-bus | the five-state test's stub bus dropped | five-state views, palette check |
|
||
|
||
### Gate, round 2
|
||
|
||
`gate.sh` on a worktree at d64f434f plus the 14 files, manifest 14 OK,
|
||
Docker 29.7.2 up, 2026-10-10T21:10:20Z to 21:15:34Z: webui 39/0;
|
||
business 60/0, bus 67/0, cli 66/0, control-board 124/0, conversation
|
||
182/0, discord 178/0, ledger 78/0, mosaic 69/0, queue 149/0, runs 41/0,
|
||
seat 19/0, tasks 51/0; build-tokens --check 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 14/0, test-task 98/0. `core.hooksPath` unset.
|