Candidate on base a3f6e9e6, not committed here. Gate green 21:27:35Z-21:30:19Z;
mutants 30/35 killed, 5 equivalent guards explained in build.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
173 lines
9.9 KiB
Markdown
173 lines
9.9 KiB
Markdown
# Row 95 (#1589): the Board shows the stack's seats, and a sign-out line
|
||
|
||
Filbert, 2026-10-12. Brief: `docs/plans/2026-10-12_pm-test-fixes.md`, "The
|
||
Console's Board shows the stack's seats and a sign-out" (blob ae867dfa,
|
||
unchanged). Darkwing reviews. Sage commits after the gate. Nothing in the
|
||
candidate is committed, staged or pushed.
|
||
|
||
Base a3f6e9e6. Nothing has landed on the manifest paths since: `git diff --stat
|
||
a3f6e9e6..f9db19d7` on `packages/webui/src/public`, `packages/webui/tests`,
|
||
`packages/webui/README.md` and `serve.mjs` is empty. `build.patch` applies at
|
||
a3f6e9e6 with `git apply --index`, and `candidate-manifest.sha256` pins all 10
|
||
files after the patch. `serve.mjs` is unchanged: the held roles come from the
|
||
existing `/api/bus/agents` read.
|
||
|
||
## What ships
|
||
|
||
- **Held roles first.** The Board's first section, "Held roles", lists each
|
||
role a run holds on the bus, from the `agents` read: role, state (with the
|
||
working count), attention, address, holder run with its harness, and held
|
||
since. A claim with no address shows "no address". It's read with the Inbox
|
||
count on every Board render (the 10 s poll and Refresh). A refusal shows no
|
||
roles; any other failure keeps the last read, labelled with its time, as the
|
||
bus pages do. Focus stays on the same Message button or Retry across the
|
||
repaint.
|
||
- **Message.** Each row has a Message button when the Console takes a send
|
||
(`send` in `/api/config`). It sets the form's role, goes to `#/inbox` and puts
|
||
the caret in the message box. Without `--send` there is no button, and the
|
||
section says messaging needs a Console started with `--send`.
|
||
- **Show machine sessions.** A checkbox in the Board's head, off by default.
|
||
While it's off the page never requests `/api/board` (no first read and no
|
||
poll), and the projects tree, Waiting on you, Seen, All sessions and the
|
||
board banners are hidden. Turning it off drops the scan, the selection and
|
||
any open conversation, so turning it on starts from a fresh read. The choice
|
||
is kept in `localStorage` as `mosaic-console-board` = `{"machine":bool}`.
|
||
- **Signed out.** Every Console fetch in `app.js` and `bus.js` passes
|
||
`redirect: 'manual'`: the board and conversation reads, the board's Seen and Reply POSTs,
|
||
`/api/config` (both), the bus and module reads, and the send. The first
|
||
`opaqueredirect` dispatches `mosaic-signed-out`. `app.js` sets
|
||
`body.signed-out`, and both scripts then:
|
||
- stop their timers and make no further Console fetch (each fetch helper
|
||
checks first, and Refresh and Pause are disabled);
|
||
- drop everything drawn from a read: the sessions, the inspector, any
|
||
conversation, the held roles, the kept bus reads and the bus page;
|
||
- show one line, "Signed out. Reload the page to sign in.", focused only if
|
||
focus was lost, never pulled out of the send form.
|
||
|
||
The send form stays with its draft and its receipt. A send answered with a
|
||
redirect says "Not sent … the sign-in proxy redirected it …; the draft is
|
||
kept". A send after sign-out says "Not sent … signed out; reload the page to
|
||
sign in again" and fetches nothing. A read in flight at the sign-out is
|
||
dropped when it lands.
|
||
- **README**: a new section, "Board: held roles and signing out (row 95,
|
||
#1589)", the "Not sent" bullet's redirect case, and the new test file in
|
||
Verify.
|
||
|
||
## Tests
|
||
|
||
- New `tests/board-held.test.mjs`, two browser tests against an in-process
|
||
broker with pm (address `pm-run`) and coder (no address) holding claims:
|
||
1. Held roles with machine sessions off: the two rows, "no address", the
|
||
count, the Message labels; the toggle unchecked, no `/api/board` request;
|
||
on, the sessions show and the setting is kept across a reload; off, the
|
||
sessions go and a reload reads no board; Message for each role lands in
|
||
`#/inbox` with that role and the caret in the message; with no send, no
|
||
Message button. Every request asked for `redirect: 'manual'`.
|
||
2. A redirect answer on a Board read (`/api/board` on Refresh), a bus read
|
||
(`/api/bus/agents` on Refresh), a conversation read (`/api/conversations`
|
||
from History), a board POST (`/api/seen` from the inspector's Seen), a
|
||
module read (`/api/queue` on the Queue page) and a send. Each time: the
|
||
line once, no NetworkError, "did not answer" or "not read" text (the
|
||
footer stays visible), no sessions or held roles left, Refresh disabled,
|
||
and no further request on a route change, Refresh, the toggle twice or
|
||
the poll. The send keeps its draft and form.
|
||
- `send-browser.test.mjs`: after its redirect-send case, the page is signed
|
||
out; the test checks the line and that a further send fetches nothing, then
|
||
reloads before its remaining cases.
|
||
- `browser-edge.test.mjs`: with storage denied, the toggle starts off and still
|
||
turns on.
|
||
- `browser.mjs`: `browser({ machine })`, default on (see choice 5).
|
||
- `shell.test.mjs`: the 1440 short-page check is 1000 px tall (choice 6).
|
||
|
||
## Choices
|
||
|
||
1. **"Kept in the page's settings" is `localStorage`.** The other Board
|
||
filters (Hide offline, Hide seen) aren't kept across a reload, so "like the
|
||
other Board filters" can't mean their storage. I read it as: a checkbox with
|
||
the other filters, kept the way the page keeps its appearance settings
|
||
(`mosaic-console-appearance`). Refused storage starts it off.
|
||
2. **No board read while off.** Hiding the sessions while still polling the
|
||
scan would spend a 1.3 s read every 10 s on data nobody sees. Off means no
|
||
request.
|
||
3. **Message only with `--send`.** The brief says Message opens the send
|
||
form; the form exists only on a Console that takes a send. A button that
|
||
leads to a page saying "this Console only reads" is worse than a line on the
|
||
Board saying so.
|
||
4. **The send form stays when signed out.** "Doesn't show stale data as
|
||
current" covers the reads. The draft is the person's own text, so it stays,
|
||
and a further Send says why it wasn't sent instead of fetching. A board
|
||
Reply in the inspector goes with the rest of the Board: its draft isn't
|
||
shown again (the README says so).
|
||
5. **`browser()` defaults to machine sessions on.** The existing suites open 16
|
||
browsers in 11 test files, and most of them read the board scan. Seeding
|
||
the setting is one line in the harness; the alternative was a click in each. Row 95's own first test passes
|
||
`{ machine: false }` to see a first visit.
|
||
6. **The shell check is 1000 px tall.** It asserts a short Board doesn't
|
||
scroll. With the held roles added, its two-session Board is 967 px, so 900
|
||
failed. The 420 px case still checks a scrolling page.
|
||
7. **The in-flight read race.** Test 2 found that a `/api/board` read in
|
||
flight when a bus read signed the page out landed afterwards and redrew the
|
||
sessions. `accept()` and `error()` now return when signed out; mutants 10
|
||
and 11 cover them.
|
||
8. **Overlap with row 77.** Row 77 (#1571, round 2 in review) also edits
|
||
`packages/webui/README.md`, in other sections. Whichever lands second needs
|
||
a rebase of that file.
|
||
|
||
## Gate
|
||
|
||
Run in my worktree (`a3f6e9e6` plus the candidate), sequential,
|
||
`DOCKER_HOST` at a dead socket, 2026-10-12T21:27:35Z–21:30:19Z
|
||
(`gate/summary.txt`):
|
||
|
||
- webui 86/0, control-board 124/0;
|
||
- every `scripts/test-*.sh` exited 0: auth 15, conductor 17, config 24,
|
||
discord 66, extension-package 18, foundation 44, queue 27, release 4 and
|
||
task 26. test-queue skipped its canonical-root part (a worktree), and
|
||
test-release skipped its Docker cases.
|
||
|
||
The first gate (20:22:51Z–20:25:43Z, same counts, `gate1/`) ran before the
|
||
first mutant round. That round left 9 of 35 alive (`mut/out-round1.txt`). Four
|
||
were gaps:
|
||
- no test redirected a conversation read, a board POST or a module read, so
|
||
dropping `redirect: 'manual'` from `getJSON`, from `api`'s POST or from
|
||
`fetchApi` survived;
|
||
- dropping `error()`'s signed-out guard survived. Without it, a redirected
|
||
Board read writes "Board not read: the read failed" into the footer, which
|
||
stays visible when signed out, and the test didn't look there.
|
||
|
||
Test 2 now redirects `/api/conversations` from History, `/api/seen` from the
|
||
inspector's Seen and `/api/queue` on the Queue page, and checks the page for
|
||
"not read" and "last known data". `app.js` and `bus.js` didn't change, and
|
||
`board-held.test.mjs` is the only file whose digest moved.
|
||
|
||
**Mutants** (`mut/mutants.py`, `mut/out.txt`), round 2,
|
||
21:03:18Z–21:27:35Z: 35 edits to `app.js` and `bus.js`, each matching once,
|
||
each run against board-held, send-browser, browser-edge and browser. 30 were
|
||
killed and both files were restored byte for byte. The 5 survivors are
|
||
redundant guards behind another check that holds:
|
||
|
||
| Survivor | Why nothing changes |
|
||
| --- | --- |
|
||
| 13 `schedule` polls with the toggle off | `refresh()` returns on `!machine` (12, killed) |
|
||
| 14 `refresh()` without its signed-out guard | `api()` throws before fetching, and `error()` returns on `err.signedOut` (11, killed) |
|
||
| 27 bus `schedule` polls signed out | `render()` returns on `out()` (26, killed) |
|
||
| 28 Board render without its signed-out return | the listener's `gen++` returns the render before any paint; only `last` is refilled, and nothing reads it again |
|
||
| 29 bus view render without its signed-out return | the same `gen++` check, before the view is drawn |
|
||
|
||
I kept them, so that no single check stands between a signed-out page and a
|
||
fetch or a stale paint. Mutants 10 and 11 (choice 7) are killed.
|
||
|
||
The mutant 19 run (toggle on reads nothing) times out at 900 s in both rounds.
|
||
The run's timeout killed only `node --test`, so its file runner and browser
|
||
were left, one per round. Darkwing saw them, and I killed both by PID at
|
||
21:23:55Z. The `mutants.py` here starts each run in its own process group and
|
||
kills the group on a timeout. That's the only change from the copy that
|
||
produced `out.txt`.
|
||
|
||
## Not tested here
|
||
|
||
- The real Firefox NetworkError: the tests stub `fetch` to answer an
|
||
`opaqueredirect`, which is what Chromium and Firefox both return under
|
||
`redirect: 'manual'`. Live, a pod replacement at the edge is the check.
|
||
- Live after the Console restart Mos approves (decision 109).
|