Built by a Haiku subagent (decision 129). cli 133/0, nine scripts green, 7 of 7 mutants killed. Manifest 5c979e49. Co-Authored-By: Claude Opus 5.5 <[email protected]>
9.0 KiB
Row 99 (#1593) build, round 1
Phase A: built and packeted. No test has been run. The only check was
node --check (a parse, no test code runs) on both edited files, which passed.
- BASE:
4233534da82ac15119358722eb6182c73cfd75b1(base.txt) - Owned paths changed:
packages/cli/src/cli.mjs,packages/cli/tests/cli.test.mjs(files.txt). Nothing else in the worktree changed. - candidate-manifest.sha256 sha256 (of the file itself):
3c3f5629a792b4a052d6c2c6e1bb2d5184109dca7537c3c75aff5089e278f0ad
What changed and why
packages/cli/src/cli.mjs, the inbox branch of human(). The messages
paging loop now runs inside a try. In text mode, when the error's code is
unknown-verb, the branch prints formatInbox(decisions), a blank line, and
messages: not available until the bus restarts, then returns, so the exit is 0.
Any other error is rethrown unchanged. The --json branch returns before the
loop, so it is unchanged.
The code check is e.code, the same pattern resolveDecision uses for
outcome-unknown. humanTransport sets error.code from the bus error on
stderr (transport.mjs:62-63). The stub in helpers.mjs sets error.code the
same way. unknown-verb is the broker's code for an unrecognized verb
(broker.mjs:574, :1019).
packages/cli/tests/cli.test.mjs: one import (formatInbox), four helpers
(inboxStub, coded, unread, NO_MESSAGES_VERB), and four tests after the
existing inbox test. All use a stub transport; none touch the real bus.
Choices I made
- Blank line before the unavailable line. The normal path puts a blank
line between the decisions and the
messages:line, becauseformatInboxends with a newline and the normal path adds\nmessages:. The fallback does the same, so the two outputs match. The brief says "then the line", and this is the one place I read the layout. Removing the blank line is a one-character change if Sage wants it. - A later page answers
unknown-verb. This means the bus restarted between pages. The partial list from earlier pages is dropped, and only the fallback prints. A partial count printed as the full count would be wrong. Test T_D covers it; mutant M6 breaks it.
Acceptance points and their tests
| Acceptance point | Test (name as it appears in TAP) |
|---|---|
unknown-verb from messages: decisions as formatInbox prints them, the line, exit 0 |
T_A: "text-mode inbox on a bus without the messages verb prints the decisions and the unavailable line, exit 0" |
any other error from messages still fails, and prints nothing (codes unauthenticated, invalid-response, outcome-unknown) |
T_B: "text-mode inbox still fails on any other messages error, and prints nothing" |
normal path: unread count and each unread message, paged with after, read messages left out |
T_C: "text-mode inbox prints the unread count and every unread message across pages" |
choice 2: later page unknown-verb drops the partial list |
T_D: "a later page answered unknown-verb drops the partial list and prints only the unavailable line" |
--json unchanged |
Existing test "inbox lists only decisions routed to the human..." (cli.test.mjs, first test), which asserts the --json output and the call order inbox, messages, inbox. Not changed. |
Mutants (planned, not run)
All edits are to packages/cli/src/cli.mjs. Each old string occurs exactly
once in the candidate (checked with grep -F -c). The runner in mutants.py
applies each one, runs cli.test.mjs, and requires the named test to show
not ok.
| Mutant | Before | After | Named test that must die |
|---|---|---|---|
| M1 remove the fallback | if (e.code !== "unknown-verb") throw e; |
throw e; |
T_A |
| M2 widen to every error | if (e.code !== "unknown-verb") throw e; |
if (false) throw e; |
T_B |
| M3 change the printed line | messages: not available until the bus restarts |
messages: unavailable |
T_A |
| M4 drop the decisions in the fallback | ${formatInbox(decisions)}\nmessages: not available |
\nmessages: not available |
T_A |
| M5 fallback exits nonzero | return io.stdout.write(...fallback...); |
{ io.stdout.write(...fallback...); throw new CliError("messages not available", 1); } |
T_A |
| M6 partial list printed as the count | the fallback return io.stdout.write(...) |
return print({ decisions, messages }, ...), the normal text output |
T_D |
| M7 read messages counted as unread | messages.push(...page.messages.filter(m => !m.read)); |
messages.push(...page.messages); |
T_C |
A mutant counts as killed only when the run exits nonzero, does not time out,
and the named test is not ok. A timeout counts as a failure, not a kill, as
the brief requires. The runner gives each run its own process group, kills
that group afterwards, and restores cli.mjs in a finally block, then checks
the restored hash.
Planned phase B test commands
Run sequentially, each with DOCKER_HOST=unix:///nonexistent.sock, output tee'd
into out/, from the worktree root:
node --test 'packages/cli/tests/*.test.mjs'(the cli node suite, aspackages/cli/README.mdgives it)- Every
scripts/test-*.sh:test-auth.sh,test-conductor.sh,test-config.sh,test-discord.sh,test-extension-package.sh,test-foundation.sh,test-queue.sh,test-release.sh,test-task.sh python3 -I mutants.py <worktree> <packet>/out. It runs the unmutated baseline first and stops if the baseline is not green.
Open questions for Sage
- Is the blank line before the
messages:fallback line right, or should the line follow the decisions directly? (Choice 1.) - Is dropping the partial list on a later-page
unknown-verbright? The other option is to print the partial messages with a note that the list is incomplete. (Choice 2.) - Scripts: the brief says
scripts/test-*.sh. I listed the nine.shfiles only. The.mjsand.pyfiles namedtest-*are not in the gate list. Say if they are wanted.
Round 2: T_A correction (test-only)
Step 1 of phase B (out/step1-cli-suite.log, kept) ran T_A and it failed with task-required, not from the CLI code. T_A raised git.push.protected with blocking: true and no task_ref. The broker refuses a blocking decision without task_ref (packages/bus/src/broker.mjs:657), so the test never reached main().
Fix: packages/cli/tests/cli.test.mjs line 65 now passes task_ref: "vikunja:1/7", the same as the other blocking raises in that file. Nothing else changed. packages/cli/src/cli.mjs is unchanged, so the mutant guard hash still holds.
Hashes after the fix:
packages/cli/src/cli.mjsafcb9d9276c96e543cdc4dac1856b2dd6e973dfa2f1c68d179ddb5fc0e25b1ad (unchanged)packages/cli/tests/cli.test.mjsc018997949b425e3274aaf9cc0877e5cd1419a1901e070ac84d114d788a53f7f (was 9b84d533374809c1ed1e3e748fb56a3351fdd4cf5d2b35f2f8ab5700631edac2)candidate-manifest.sha256(the file itself) 5c979e49d0b8433b7479fb8faf0c254012b4e76e7f39cb0ae0d4013995e4df60 (was 3c3f5629a792b4a052d6c2c6e1bb2d5184109dca7537c3c75aff5089e278f0ad)
Regenerated: build.patch (git diff against base.txt), candidate-manifest.sha256. The round-1 copies are not kept. The round-1 log is kept at out/step1-cli-suite.log. sha256sum -c passes.
Environment: node_modules is a symlink to the canonical checkout, per Sage. It is untracked and not part of the candidate. Remove the link before the worktree is removed.
Round 2: phase B results (candidate unchanged from the T_A fix)
- Step 1, cli node suite (out/step1-cli-suite-r2.log): 133 pass, 0 fail, 0 cancelled, exit 0. The round-1 log is kept as out/step1-cli-suite.log.
- Step 2, the nine suites: 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 (1 skip: render check, this is a worktree, not the canonical root), test-release 14/0, test-task 26/0 (1 skip: live recall, docker daemon unavailable under DOCKER_HOST). All exit 0. Logs: out/test-*-r2.log.
- Docker: test-release created
agent-a37ace32df0dd168f_default, removed after it withdocker network rm. - Step 3, mutants: baseline green. M1 to M7 killed (named killers in out/results.json).
Runner defect found and fixed (mutants.py only, no candidate change)
First mutant run (kept: out/mutants-run1/, mutants.py.run1 beside mutants.py): M6 reported NOT killed. Cause: its replacement text used join(\"\") inside a raw string, which produces invalid JavaScript (SyntaxError at cli.mjs:189). The module never loaded, so T_D never ran. The file-level not ok 1 - packages/cli/tests/cli.test.mjs was the only failure, and the runner correctly refused to count it. Fix: line 82 now uses join(''). The rest of the runner is unchanged.
Second run, after the fix: M1 to M7 all killed, baseline green, cli.mjs restored to afcb9d9276c96e543cdc4dac1856b2dd6e973dfa2f1c68d179ddb5fc0e25b1ad.
- M1 killed by T_A, T_D
- M2 killed by T_B
- M3 killed by T_A, T_D
- M4 killed by T_A, T_D
- M5 killed by T_A, T_D
- M6 killed by T_A, T_D (T_A fails because the fallback text changed)
- M7 killed by T_C