Files
stack/agents/darkwing/work/queue-55/BUILD.md
T
jason.woltjeandClaude Opus 5.5 26926d183a docs(queue-55): row 55 round 3 candidate packet, R3 and B1 (darkwing)
One rule for Filbert's R3 and Dewey's B1: y resolves only while the
whole decide box is shown. Adds N9 widths, the start-unguarded and
timeout fixes, and two defects found by the cheap tests (wrap at
graphemes, answer render throw). 51/51 mutants killed; gate on
122b1223 green, cli 110.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 14:59:13 -05:00

547 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Row 55 (#1544): the full-screen terminal app, candidate packet, rounds 1, 2 and 3
Author: Darkwing. Reviewers: Dewey and Filbert. Brief:
`docs/plans/2026-10-10_design-implementation.md`, section "Full-screen
terminal app", with `docs/design/IMPLEMENTING.md` "The full-screen
terminal app" and "Strings to keep exactly". Base: `fe5c68fa` (row 41,
`base.txt`). It applies unchanged to `23d566d6`: the commits between the
two touch only BUILD-LOG.md, docs/SESSIONS.md and the queue files, none of
them in this candidate. The feature is uncommitted; only this packet is
committed. I pushed nothing.
## Files
`files.txt` lists the nine repository paths. `candidate-manifest.sha256`
has their sha256. `build.patch` is `git diff --cached --binary fe5c68fa`
over them: 9 files, +1363/−16.
| File | Change |
|---|---|
| `packages/cli/src/app.mjs` | new: key parsing, the `App` state machine, `runApp` (alternate screen, raw mode, resize, SIGTERM, restore) and the quit line |
| `packages/cli/src/screens.mjs` | new: `draw(state)` is pure and returns rows; Inbox, Tasks, Agents, Trail, Help, overlays, the 16-colour palette and NO_COLOR |
| `packages/cli/src/cli.mjs` | the decide flow moves into the exported `resolveDecision`, which `decide` and the app share; `SEAT_USAGE`; the no-argument entry `app()` |
| `packages/cli/src/format.mjs` | `trailSummary` is exported (the Trail screen uses it); no other change |
| `packages/cli/tests/app.test.mjs` | new: 14 tests, listed below |
| `packages/cli/README.md` | the intro names row 55; new section "The app" |
| `docs/TOOLS.md` | the `mosaic` entry: the no-argument line and a four-line paragraph |
| `scripts/mosaic` | a decided scope addition (Sage, 2026-10-11); see below |
| `packages/queue/tests/dispatch.test.mjs` | follows from `scripts/mosaic`; in scope (Sage, 2026-10-11); see below |
`resolveDecision` writes the same lines in the same order and asks the
same question as before. `decide` passes `ask = null` for `--yes` and
keeps its own "stdin is not a terminal" refusal. The 83 tests that were in
`packages/cli/tests/` at the base still pass unchanged.
### Scope addition: scripts/mosaic
`scripts/mosaic` is not in the brief's files. Sage approved it on
2026-10-11 as a decided scope addition. Before, `scripts/mosaic` with no
argument fell through to `packages/seat/src/cli.mjs`, which printed
`refused: <seat usage>` and exited 4. The script never reached
`packages/cli`, so the brief's acceptance line, bare `mosaic` on a TTY
opens the app, could not be met inside `packages/cli` alone. The change is
one case that sends no argument to `packages/cli/src/cli.mjs`. Nothing
else in the script changed.
The entry checks in Sage's order:
1. Agent markers exit 3 through `refuseInsideAgent`, with the transport's
message unchanged.
2. stdin or stdout not a TTY exits 4 with the usage.
3. Otherwise the app opens.
Test 11 below covers each branch through `main([])`. Test 12 spawns
`scripts/mosaic` itself for exits 4 and 3.
### The queue dispatch test
My first gate run had test-queue red, 26 passed and 1 failed
(`out/round1-first-gate/test-queue.log`). `packages/queue/tests/dispatch.test.mjs`
pins A2's rule that every call but `queue` reaches the seat CLI exactly as
the pre-A2 script did, and its list of calls included `[]`. Sage's approved
change sends exactly that call somewhere else, so the pin can't hold as
written. The file is outside the brief. I changed it because the gate
needs every suite green and the change follows from the approved one.
Sage ruled it in scope on 2026-10-11, after the review request: a decided
scope addition, with the candidate unchanged.
The change, 16 lines in and 5 out:
- `[]` leaves the list of seat calls. `[""]` stays: it is one argument, so
it still goes to the seat CLI.
- The fake root gains a fake `packages/cli/src/cli.mjs`.
- A new test: no argument reaches `packages/cli/src/cli.mjs` with empty
argv, the same working directory and stdin, and the fake's exit code
and output passed through. The same call on the pre-A2 fixture still
reaches the seat CLI.
- The header comment names row 55.
With `scripts/mosaic` put back to the base, the new test fails and the
other three pass. With the candidate, all four pass. The test that A2 only
adds lines to the pre-A2 script still passes, because the row 55 case adds
four lines and removes none.
## How the brief maps to the candidate
| Brief | Where | Test |
|---|---|---|
| no argument on a TTY opens the app | `cli.mjs` `app()`, `app.mjs` `runApp` | 11, 12; `out/smoke.txt` |
| not a TTY: usage, exit 4 | `cli.mjs` `app()` | 11, 12 |
| agent run: exit 3 | `refuseInsideAgent` in `transport.mjs` | 11, 12 |
| Inbox, Tasks, Agents, Trail, Help; keys c i t a r j k : ? q | `app.mjs` `#key`, `screens.mjs` | 1, 9 |
| decide runs the existing flow, strings unchanged | `resolveDecision` | 4, 5, 6, 7, 13 |
| 16 colours, NO_COLOR, 80x24, designed at 120x36 | `screens.mjs` | 1, 3 |
| no new dependency | `package.json` untouched | |
| every screen fits 120 columns and 80x24 | | 1 |
| NO_COLOR output has no escape codes | | 3 (see deviation 1) |
| the string grep test | | 13 |
The tests in `packages/cli/tests/app.test.mjs`:
1. Every screen and overlay fits 120x36, 80x24 and the narrow layout
(100x30, 99x24), row for row, in colour and NO_COLOR. That covers every
view and overlay, plus the empty, stale, refused and no-business states.
2. Broker text is shown, never interpreted. No raw control character or
bidi override reaches the terminal (`visible()` from
`packages/conversation/src/terminal.mjs`).
3. NO_COLOR: no escape codes except reverse video (7/27) on the selected
row. A frame with nothing selected has none.
4. 1-9 shows the decide flow's own lines and question. Only y resolves,
and it goes through the test broker.
5. The app's decide text equals `mosaic decide` stdout for the same
decision.
6. n, N, Esc, Enter and q at the question cancel. Other keys wait.
Nothing is sent.
7. Outcome unknown shows the CLI's sentence and offers no retry. Then v
opens the trail, or r reads again.
8. Refused and failed reads keep the last list, marked stale. A refused
decide says refused.
9. Keys: views, moving, the trail of a selection, the command line, help
and quit.
10. No business: the app says so, and `:business <id>` picks one.
11. `main([])`: exit 3 with a marker set, exit 4 with the usage off a
terminal, and the app on one (stubbed `runApp`).
12. `scripts/mosaic` with no argument reaches the app entry (exit 4 off a
terminal, 3 with a marker). `SEAT_USAGE` still equals what the seat
CLI prints, so the copy can't drift.
13. Strings to keep exactly appear once, in `cli.mjs` and `transport.mjs`.
`app.mjs` and `screens.mjs` don't restate "authorizes the action",
"declines the action", "[y/N]", "may have recorded it" or "runs only
from a human shell".
14. `draw` is pure: the same state gives the same frame.
## Deviations, stated
1. **NO_COLOR keeps reverse video on the selected row.** The brief's test
line says NO_COLOR output has no escape codes. `IMPLEMENTING.md` line
188 says reverse video stays for the selected row, and
`docs/design/README.md` line 148 makes reverse video the selection
mark. I followed the design. Test 3 checks that 7 and 27 are the only
SGR codes, and that a frame with no selection has none. The writer's
`\x1b[H\x1b[2J` before each frame and the alternate screen codes are
cursor and screen control, not colour.
2. **The non-TTY message gains one line.** It prints
`mosaic: the app needs a terminal on stdin and stdout; from a script,
use a verb:` and then the seat usage and the `packages/cli` usage, exit
4. Before, it printed `refused: <seat usage>`. `IMPLEMENTING.md` line
191 says to refuse and point at the verbs, and the old text named only
the seat verbs. If a reviewer wants the old bytes, test 12 already
holds them as `SEAT_USAGE`.
3. **The Terminal class is not the base.** The brief says the app is built
on the conversation `Terminal` class. Only `visible()` is imported.
`Terminal` is a line editor for Talk, and Talk is out of scope here. The
app needs a whole-screen redraw and single-key input, which `Terminal`
doesn't do. When Talk joins, the Talk screen embeds `Terminal`.
4. **Reads block redraw.** `humanTransport` uses `spawnSync`, so the
screen doesn't redraw during a read (30 s timeout). That is the existing
transport, unchanged. The README says so.
## Evidence, round 1
All runs in a detached worktree of `23d566d6` with `build.patch` applied
and `node_modules` linked (`out/gate/`).
- `packages/cli`: 97/97 (83 before, plus the 14 new), `out/gate/cli-tests.log`.
- Every `scripts/test-*.sh`, run in sequence on the final candidate, all
exit 0 (`out/gate/summary.txt`, logs beside it):
| Suite | Result |
|---|---|
| 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 | 27 passed, 0 failed; canonical-root checks skip in a worktree |
| test-release | 14 passed, 0 failed |
| test-task | 26 passed, 0 failed; 4 Docker groups skipped |
test-task ran with `DOCKER_HOST` pointed at a missing socket, so its
adapter, workspace, live task and live recall groups printed skip. The
live ones call a real model. Nothing this candidate changes runs in
those groups. Sage's gate rerun covers them with Docker.
- The first gate run, before the dispatch test change, is kept in
`out/round1-first-gate/`. `out/dispatch-mutant.txt` has the dispatch
test against the base script and the candidate script.
- Smoke, `smoke.sh`: the app in a pty at 80x24 against a fixture config,
never the real bus. Keys ? Esc t q. The alternate screen was entered once
and left once, then it printed `(app closed with q; 0 decisions resolved
this session)` and exit 0 (`out/smoke.txt`).
- `out/apply.txt`: `build.patch` applies to `23d566d6` in a fresh
worktree and the manifest matches.
## Follow-ups (not in this row)
- The transport's "did not finish" `CliError` (`transport.mjs` line 60)
has no `.code`, so the outcome-unknown branch in `resolveDecision` does
not see it. `mosaic decide` then prints
`outcome-unknown: the bus transport did not finish`, exit 1, instead of
the "may have recorded it" sentence. The app matches the message text
and shows its outcome-unknown overlay, so only the verb has the gap. It
predates this row. Written up in `FOLLOWUP-transport-code.md`; Sage
queues it with the 2026-10-12 batch.
- Talk and stop in the app, Decision Seen, and the web console stay out of
scope, as the brief says.
## Round 2
Round 1 drew changes requested from both reviewers: Filbert in comment
27251 (rev 390) and Dewey in comment 27253 (rev 395), both on candidate
`93fa48c1cdfe`. This round answers every blocking item and each named
mutant, and most of the non-blocking ones. `build.patch`, `files.txt`
and `candidate-manifest.sha256` now hold the round 2 candidate. The
round 1 versions are in this packet's history at `4acadaae`. The same
nine files: +1741/−16. Base for the apply check: `c506c6ec`
(`out/gate-round2/apply.txt`). None of the commits since `fe5c68fa` touch the nine
files.
### Blocking
**R1 (Filbert): the question took type-ahead.** Now y or Y resolves and
any other key answers No. Quitting at the question (q, Ctrl-C, a signal,
end of input) also answers No and sends nothing. Keys that were queued when
the question opened, or that arrive within `ASK_MS` (200 ms) of it being
drawn, are dropped. The timer arms the question; arming empties the key
queue. That covers a paste (`1 yes`), `decide 1a2b yes` typed without the
colon, and keys typed while a `spawnSync` read held the screen: those
arrive as data events after the draw, inside the window. A timer refresh
tick is never taken as an answer. The hint now reads "y resolves · any
other key cancels (the default is No)".
200 ms is my choice. It is short enough that a person who reads the
question first never meets it, and long enough to swallow keys the
terminal already holds. A y pressed inside it is dropped silently; the
next one counts. Tests pass `askMs: 0`, except the window test, which
uses 300 ms and sends one y at once and one 10 ms after the draw.
**R2 (Filbert): `cells()` miscounted emoji.** `\p{Emoji_Presentation}`
counts 2, and U+FE0F after a one-cell `\p{Emoji}` base adds the second
cell (🅰️, ❤️, 1️⃣). The old range 1F300-1FAFF became Emoji_Presentation
plus 1F200-1F2FF. `width`, `fit` and `wrap` pass the previous code point.
The fake inbox now carries ✅✅, ⚡ and 🅰️; a task title carries 発行 ✅.
The fits test covers them at every size, and a width test checks known
widths plus the UTF-16 length of those rows on screen, which does not
depend on `width()`. Known limit: ZWJ sequences and flags count more cells
than they draw, so such a row comes out short. It can't overflow.
**B1 (Dewey): a tall decide box cut the authorization lines.** The box now
cuts inside the question. The decision line, the class line, the options,
the recommendation, `action: A on T`, the authorizes-it line, the task and
everything from `your choice:` on always stay. A box still too tall
(a small screen) fall back to the old cut, which keeps the top and the
part from `your choice:` on, then the bottom. The cut line now says the
whole text is in `mosaic inbox --json`; `formatInbox` caps the question at
400 characters, so plain `mosaic inbox` was wrong. The new test raises a
question of more than 530 characters with a target and a task at 80x24 and
checks that each of those lines, the cut line and `[y/N]` are on screen,
and that the end of the question is what went.
### Filbert's mutants
`out/mutants-round2/` has the runner and its output: 30 mutants, each one
edit to `src/`, with the whole `packages/cli` suite run against it. All 30
are killed. `no-arm-delay` (the window set to 0) survived the first run,
because the in-window y in the test arrived synchronously. The test now
also sends one 10 ms later, and the rerun kills it.
| Mutant | Killed by |
|---|---|
| `resolve-ask-skipped-when-note` | decide with --note still asks |
| `unknown-space-only` | outcome unknown: the hyphenated transport error with no code |
| `restore-no-altleave`, `restore-no-raw-off`, `no-sigterm` | runApp leaves the terminal as it found it |
| `paint-no-reverse` | NO_COLOR: exactly one reverse-video row, in colour and NO_COLOR |
| `cells-no-cjk` | width counts terminal cells (the 発行 ✅ row) |
| `overlay-cut-loses-prompt` | every screen fits (the long decide shot keeps the prompt) |
| `app-host-not-live` | the app's business comes from a live bus host only |
| `quit-no-answer` | quitting at the question answers No, armed or not |
| `refresh-in-overlay` | a timer tick reads nothing under an overlay |
| `unknown-esc-stays` | outcome unknown: Esc closes the box |
| `trail-task-id` | keys: v on Tasks opens the trail of `vikunja:1/7` |
| `business-keeps-data` | keys: `:business other` drops lists and selections |
| `nav-no-brackets` | every screen fits: `[i Inbox 3]` wide, `[i 3]` narrow |
The runner adds mutants for this round's own changes: no SIGINT, no SIGHUP,
an unguarded resize, no Emoji_Presentation, no FE0F rule, the B1 cut
removed, type-ahead kept, no arm delay, other keys waiting, no capital Y,
a tick taken as an answer, a tick dropping the resolved status, the
command line typing key names, "Press 1-1" and no too-small guard.
### Non-blocking
- **N1 (Filbert; Dewey concurs).** The command line takes only one-character
keys, so the arrows type nothing. Tested.
- **N2.** `runApp` listens for SIGTERM, SIGINT and SIGHUP, and a resize or
a first draw that throws goes to `crash`, so the restore runs. Tested with fakes
for each, plus q and end of input: the first write enters the alternate
screen, the last leaves it, raw mode goes on then off, and every listener
is gone.
- **N3.** Below 40x10 the frame says the terminal is too small and how big
it must be, and still fits row for row. Tested at 39x10, 40x9, 3x3 and
1x1; 40x10 joins the fits sizes.
- **N4.** A timer tick keeps a "resolved" status. Tested.
- **N5.** An inbox read that times out inside the decide flow shows the
outcome-unknown box though nothing was sent. Not changed here. It is
added to `FOLLOWUP-transport-code.md`, because the fix belongs with the
transport's missing `.code`.
- **N6.** One option says "Press 1 to choose". Tested.
- **N7.** `runApp` has a test (N2 above).
- **N8.** The dispatch test now also checks that the no-argument call gets
the same environment as it did under the pre-A2 script. The old non-TTY
bytes: deviation 2 stands. If Sage wants them back, it is one line in
`cli.mjs`, and test 12 already holds the bytes as `SEAT_USAGE`.
- **Dewey: Ctrl-C on the command line.** It closes the command line; it
does not quit. The `app.mjs` header and the README now say so.
- **Dewey: REACHES YOU.** Listed below as deviation 5.
### Deviation 5
5. **The Inbox has no REACHES YOU column.** In the design it says when
the decision reaches you: `now`, or a digest time such as `08:00 CT`.
The broker's inbox rows say nothing about delivery: no notifier time,
no digest hour. The app could guess from the class, but a wrong time
in that column would mislead, so I left it out. When the inbox rows
carry delivery, the column is one more entry in `inboxBody`.
### Tests, round 2
`packages/cli/tests/app.test.mjs` has 23 tests (14 in round 1). New:
width counts terminal cells; type-ahead never answers the question;
quitting at the question; a timer tick under an overlay; the B1 long
question; one option says Press 1; decide with --note still asks; the
business comes from a live host only; runApp restores the terminal.
Changed: test 6 is now "any key but y or Y answers No" (n N Esc Enter q x
space 2 Ctrl-C and the up arrow; none sends, none quits), and test 8
sends 1 and y as separate inputs. Tests 1, 3, 4, 7 and 9 gained the
assertions named above.
### Evidence, round 2
All runs in a detached worktree of `c506c6ec` with the round 2
`build.patch` applied and `node_modules` linked. `out/gate-round2/` has
the logs, `summary.txt` and `gate.sh`, the script that ran them in
sequence.
- `packages/cli`: 106/106, `out/gate-round2/cli-tests.log`.
- Every `scripts/test-*.sh`, all exit 0:
| Suite | Result |
|---|---|
| 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 | 27 passed, 0 failed; canonical-root checks skip in a worktree |
| test-release | 14 passed, 0 failed |
| test-task | 26 passed, 0 failed; Docker groups skipped, as in round 1 |
A `gate_default` Docker network appeared during the run. It had no
containers afterwards, and I removed it.
- `out/gate-round2/apply.txt`: `build.patch` applies to `c506c6ec` and
the manifest matches. The nine files are byte-identical at `fe5c68fa`
and `c506c6ec`.
- `out/gate-round2/smoke.txt`: `smoke.sh` at 80x24 against a fixture
config, never the real bus. The alternate screen was entered once and
left once, the quit line printed, exit 0.
- `out/mutants-round2/`: `run2.mjs` and its output. `round2.txt` is the
run of all 30 mutants: 29 killed, `no-arm-delay` survived. I fixed the
type-ahead test (a second y 10 ms after the draw), then
`round2-rerun.txt` reran the two type-ahead mutants: both killed. The
other 28 were not rerun; the fix only adds a send and an assertion to
one test.
- Candidate manifest sha256:
`3f2d8f8ea3fea1cf7a002297ee9b23059f1522e8060c1d981fba19c85baeabd7`.
## Round 3
Round 2 drew changes requested from both reviewers on candidate
`3f2d8f8e`. Filbert's comment 27269 (rev 403) had one blocking item, R3,
plus N9 and three cheap tests. Dewey's comment 27278 (rev 407) had one
blocking item, B1 again, plus two non-blocking ones. Dewey asked for one
rule covering B1 and R3, and this round has one. `build.patch`,
`files.txt` and `candidate-manifest.sha256` now hold the round 3
candidate. The round 2 versions are in this packet's history at
`fa6d889c`. The same nine files: 9 files, +1932/−16 against `fe5c68fa`. Base for the apply check:
`122b1223` (`out/gate-round3/apply.txt`). None of the nine files changed
between `fe5c68fa` and `122b1223`.
### Blocking: R3 and B1, one rule
**The defects.** R3 (Filbert): below 40x10 the frame shows only the
too-small text, and `y` still resolved the question it hid. B1 (Dewey):
between 40x10 and about 80x21 the decide box didn't fit, and `draw()`'s
general fallback cut it from the top and the bottom. The action, what
the choice authorizes, the recommendation and the task were dropped,
and at 80x10 and 40x12 "your choice:" too, while `y` still resolved.
**The rule.** `y` or `Y` resolves only while the frame shows the whole
decide box, with the end of a long question as the one thing it may
cut. Every other case answers No and sends nothing.
- `decideBox(o, w, room)` in `screens.mjs` builds the box. If it is
taller than the body, it cuts the end of the question and keeps the
rest: the decision line, the options, the recommendation, the action
and target, what the choice authorizes, the task, "your choice:", the
question and its keys. If those don't fit either, it returns null.
- `decideShown(o, cols, rows)` is true when the terminal is at least
40x10 and `decideBox` returns a box. It is exported and `App#ask`
uses it: `answer((k === "y" || k === "Y") && decideShown(...))`.
- `App#ask` reads the size at each key, not when the question was
armed. A terminal shrunk after the question appeared answers No, and
one grown back first lets `y` resolve.
- When `decideBox` returns null, `overlayBox` draws a small box: "Too
small to show this decision." and "Any key answers No." (or
"resolving…" when the question isn't open yet).
- Below 40x10 the too-small text ends "A decision question is open; any
key answers No." while the question is open, instead of "q quits",
since q answers No there.
- `draw()`'s general fallback now applies only to help and to a long
detail. A decide box always fits by the time it reaches `draw()`. The
fallback no longer slices: the body is at least 5 rows, so top, cut
line and last two lines are exactly that many.
The overlayBox comment and the README bullet said the must-keep lines
were never lost. Both now describe the rule and the hidden box. The
README states the sizes I measured for a gated decision with a task:
20 rows at 80 columns, 21 with a cut question, and 29 rows at 40
columns. `out/probe-round3.mjs` measured them, and its output is
`out/probe-round3.txt`. The smallest row count per width, short question
40:29, 50:26, 60:22, 80:20, 120:20; long question 40:30, 50:27, 60:23,
80:21, 120:21.
**Tests.**
- "below 40x10 the question is hidden, so every key answers it No, y
included (Filbert R3)": at 30x24 and 80x9 from the start, and at 80x24
shrunk to 30x24 after arming, `y`/`Y` sends nothing, the status is
`not resolved` and the decision stays in the Inbox. The frame shows
the new text and not "q quits". Shrunk and grown back before the key,
`y` resolves.
- "a decide box too big for the terminal says so, and every key answers
it No, y included (Dewey B1, round 3)": a gated decision with a task.
Dewey's sizes, short question at 80x18 and 40x24 and long question at
80x20, plus 120x16 and 80x10: the frame says "Too small to show this
decision" and `y` sends nothing. At 80x20 short and 80x21 long, the
smallest sizes that show it, every must-keep line is on screen and `y`
resolves. Armed at 80x24 and shrunk to 80x15 before the key, `Y`
answers No.
- "at every size the decide box shows each line y depends on, or says
it is hidden": columns 40 to 120 in steps of 4, rows 10 to 40, short
and long questions. At each size either `decideShown` is true and the
frame has all ten must-keep lines, or it is false and the frame has
the hidden box. Each side holds more than 100 sizes.
- The "every screen and overlay fits" test now expects the hidden box
for its long overlay at the narrow layout, not a cut box.
### Non-blocking
**Dewey: `start-unguarded` survived.** The runApp test now has a case
where the terminal's write throws on the first frame. runApp must
reject with that error and leave the terminal as it found it. The
`.then` handler is attached at the call, because the first frame's
error rejects before the next tick.
**Dewey: four mutants were killed only by a hang.** `{ timeout: 10000 }`
is now on "decide with --note still asks", "quitting at the question",
and the runApp test, which covers `no-sigterm` and `no-sighup`.
**N9 (Filbert).** `cells()` now counts the remaining East Asian Width W
and F ranges as 2: U+2329, U+232A, U+A960-A97C, U+FE10-FE19,
U+FE50-FE6B, U+16FE0-16FF1, U+17000-18D08, U+1AFF0-1B2FB. The width
test checks one character from each, plus U+1AFF0's range end, U+1B2FB.
**Cheap tests (Filbert).**
- U+1F210 measures 2 (width test).
- A throw from `answer`. The test makes the terminal write throw after the
question is drawn, then presses `y`. This found a defect. `answer` threw
from its `render()` before calling `resolve`, so the decide job never
ended: the pump crashed the app, but `this.job` stayed pending.
`answer` now resolves No when its render throws, then rethrows to the
pump. The test checks the quit is `an error` with that error, the job
ends, and nothing is sent.
- ❤️ at a wrap edge. This also found a defect. `wrap()` broke long words
between code points, so `wrap("abc❤️ x", 4)` gave `abc❤` and a line
starting with a lone U+FE0F. It now breaks between graphemes
(`Intl.Segmenter`), so the result is `abc` and `❤️ x`. The test also
checks a line of marks and ❤️ never comes out wider than asked.
The "runLoop never overlaps ticks" flake is in the notifier tests, not
in this candidate. I left it to the notifier's owner.
### Tests, round 3
`packages/cli/tests/app.test.mjs` has 27 tests (23 in round 2); the cli
suite has 110. New: the R3 test, the answer-throw test and the two B1
tests. The width test gained the N9 characters, U+1F210 and the wrap
checks. The runApp test gained the first-frame error case.
### Evidence, round 3
All runs were in a detached worktree of `122b1223` with the round 3
`build.patch` applied and `node_modules` linked. `out/gate-round3/` has
the logs, `summary.txt` and `gate.sh`, the script that ran them in
sequence.
- `packages/cli`: 110/110, `out/gate-round3/cli-tests.log`.
- Every `scripts/test-*.sh` exited 0:
| Suite | Result |
|---|---|
| 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 | 27 passed, 0 failed; canonical-root checks skip in a worktree |
| test-release | 14 passed, 0 failed |
| test-task | 26 passed, 0 failed; Docker groups skipped, as in rounds 1 and 2 |
A `gate_default` Docker network appeared during the run. It had no
containers afterwards, and I removed it.
- `out/gate-round3/apply.txt`: `build.patch` applies to `122b1223` and
the manifest matches.
- `out/gate-round3/smoke.txt`: `smoke.sh` at 80x24 against a fixture
config, never the real bus. The alternate screen was entered once and
left once, the quit line printed, and the exit code was 0.
- `out/probe-round3.mjs` and `out/probe-round3.txt`: the size probe the
README numbers come from. Run it with `CLI=<checkout>/packages/cli node
--test out/probe-round3.mjs`.
- `out/mutants-round3/`: `run3.mjs` and its output `round3.txt`, 51
mutants. Round 2's 30 less `overlay-cut-loses-prompt`, which edited a
line this round removed. Four re-anchored to lines this round changed
(`other-key-waits`, `no-capital-y`, `tiny-no-guard`,
`b1-cut-keeps-end`). Then 22 for round 3, among them
`r3-y-ignores-shown`, `r3-shown-is-min-size` (y checks only 40x10),
`r3-shown-at-arm` (the size read once, when armed),
`r3-no-question-whole-box` and `r3-start-unguarded`. All 51 were
killed, and the three source files matched the candidate afterwards.
One run, no fix needed.
- Candidate manifest sha256: `59f1b9b4dabfb47dc79641de209317cfd538cb27bd484b5391a0703a8c84c60b`.