(cherry picked from commit6d15eb5fc2) (cherry picked from commite553e02a75)
6.1 KiB
Row 173 (#1671): a Seen click that waited out a Board read keeps the meaning it was clicked with
Brief: docs/plans/2026-10-19_seen-click-intent.md (blob 225ac823b). Branch dewey/row173 off aca16404c.
Queue reviewer: Darkwing. Gate (queue row): Darkwing approves on #1671 naming the candidate manifest; webui and
every scripts/test-*.sh green on Sage's gate rerun.
Design
- Intent.
seenButtondrawsdata-intent="seen"on a Seen button anddata-intent="unsee"on an Unsee button, from the samer.seenthat picks the label. The click handler reads it intomarkbeforewriting = trueand before the wait, so the fresh row can't change it. I chose adata-value over reading the label text so a later relabel can't change what a click posts. - The fresh row already holds the intent (
r.seen === mark). This happens when another viewer marked or unmarked the row while the read was in flight. Nothing is posted, the button is re-enabled, and the page announces<agent> already seenor<agent> already waiting.- Why post nothing. The route takes a repeat without harm:
markSeenwithseen:truerewrites the same mark, andseen:falsedeletes a key that is already gone (control-board/src/scan.mjs:255-256). So posting the same value would also be correct. It would cost a write and a rescan, though, and the read that just finished has already drawn the row. The brief allows either. - An unseen row can still hold an old mark for an earlier lastActivity. Posting nothing on Unsee leaves that
mark, which is harmless because
scanAgentonly counts a mark equal to the row's lastActivity (scan.mjs:340).
- Why post nothing. The route takes a repeat without harm:
- Otherwise the click posts
seen: markand announces frommark, not from the fresh row. - Unchanged: the row 172 wait, the single-click guard (
writing), the no-row branch, and the/api/seenroute (out of scope). seen.disabled = falsein the new branch matches the no-row branch. After the read,acceptredraws the inspector and the cards, soseenis usually a detached button and the line has no visible effect. No test can tell it apart; see M5.
Test
packages/webui/tests/board-held.test.mjs gains one test: "a Seen or Unsee click that waited out a Board read
keeps its meaning: another viewer's mark in that read posts nothing". setup returns boardURL: f.boardURL so
the test can post to the board as the other viewer.
- Steps, for each of Seen and Unsee:
- it holds
/api/boardwithwindow.__holdand clicks Refresh; - while the read is held, it posts the other viewer's mark straight to the board;
- it clicks the inspector's button and releases the read.
- it holds
- Checks: the announcement (
agent1 already seen, thenagent1 already waiting),#seen-count(1, then 0), and that the page sent no/api/seenat all. - The held fetch hasn't reached the board when the other viewer posts (the stub awaits the hold before calling the real fetch), so the released answer carries the other viewer's mark.
Hash pins and readers checked (git grep, with a positive control for 64-hex): nothing pins app.js. shell.test
counts existing app.js strings only. browser.test, sso-browser.test and the other board-held tests click
[data-seen] buttons the page draws, so they get data-intent with them.
Mutants (run on CLEAR only; each one edit to app.js, then board-held.test.mjs whole)
| id | edit | expected kill |
|---|---|---|
| M1 | post seen: !r.seen and drop the r.seen === mark branch (the old toggle) |
new test (a post, and "returned to waiting") |
| M2 | drop the r.seen === mark branch only (post the repeat) |
new test (a post, and "marked seen") |
| M3 | seenButton swaps the intent (r.seen ? 'seen' : 'unsee') |
row 172 test (#seen-count never 1) and the new test |
| M4 | the new branch returns without announce |
new test (announcement wait times out) |
| M5 | drop seen.disabled = false in the new branch |
equivalent: the button is redrawn by accept; named, not run |
Each run: timeout -k 10 180 node --test --test-force-exit packages/webui/tests/board-held.test.mjs; a kill
counts only as an assertion or a named wait failing in the named test.
Runs
None yet. Gander holds the gate slot (row 253 run 5) from 16:32Z. The claim, the push and every run wait for Sage's CLEAR.
Round 2: rebase onto 78b69e1e6 (sage-4361)
- Branch dewey/row173-r2 off refactor
78b69e1e6. The round 1 commit25ff7cd3f(onaca16404c, 555 commits behind) was cherry-picked as a new commit; dewey/row173 is untouched. No force-push. - The pick merged on its own.
git range-diff aca16404c..25ff7cd3f 78b69e1e6..<pick>reports the commit equal. - Overlap with refactor: one app.js line, the
/api/confighandler at the end of the file (federation render). It is far from both row 173 hunks and changes no assertion or reviewed line. board-held.test.mjs and the brief (blob225ac823b) are unchanged on refactor. - No test pins app.js by hash (grepped tests for app.js plus 64-hex).
Run list
app.js is a page script, loaded by index.html, not imported by any module. Its test readers:
- by name: board-held, browser, granted-user, serve, shell, sso-browser, user-option-swap;
- by page load: every webui test that opens the Console through tests/browser.mjs (31 test files).
No test outside packages/webui reads or serves app.js (
git grepover packages/*/tests and scripts: none).
Order, each run with TMPDIR from mktemp -d /mnt/storage/scratch/tmp/d173XXXXXX and absolute paths:
- M1-M4 below, each with board-held.test.mjs whole.
- board-held.test.mjs whole on the restored tree.
- packages/webui whole (all 70 test files), which covers both reader groups. case1-accept.test.mjs is not in the list (mos-1612). Runs need Sage's CLEAR.
Mutant procedure
The M1-M5 table above stands. M5 stays equivalent: the new branch is reached only after the waited-out Board
read's accept (refresh, app.js:372) has redrawn the row, so seen is the detached old button. Each mutant is one
edit to app.js. Before the next mutant, the restored app.js must match the candidate's sha256. Each run is in its
own process group so a timeout kill takes its browser with it. Afterwards I sweep for orphaned node/chromium
processes in my run directory.