# Row 170 (#1667, P19 380): a tester's message to a role nobody holds is refused Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-19_tester-no-holder-refusal.md, blob 70e493933030 (commit 97cc7f0a3), approved in mos-0854. Assignment sage-1018; slot sage-1025. Base: 000730949 (base.txt, origin/refactor). The brief's line numbers are at 42817065e; `git log 42817065e..000730949 -- packages/bus/src/broker.mjs packages/cli/src/cli.mjs` is empty, so they still hold. Candidate: 0b6ff260f (candidate.txt), one commit on the base, in Dewey's worktree. build.patch is `git diff 000730949 0b6ff260f`. candidate-manifest.sha256 lists the 6 changed paths with their sha256. ## What ships - `packages/bus/src/broker.mjs`, `message.send`: once `#threadFor` has picked the thread, a tester's send refuses `tester-target-not-running` when `#recipient(business, to, thread)` is null. The check sits before the `INSERT INTO messages`, so a refused send writes no messages row, no delivery and no `_bus` notice. A launched run that hasn't claimed counts as no holder, as in `#waiting`. Human and agent sends are unchanged. The `#waiting` comment now says a tester's message to a role nobody holds never reaches it. - `packages/cli/src/cli.mjs`: - `talk`'s catch turns a `--tester` send refused `tester-target-not-running` into `CliError(" isn't running, so the tester's message was refused and nothing was stored", 3)`. The check needs both the code and `--tester`, so a human send is never rewritten. - The `!w.told` line in `waitingLines` is removed, along with its comment. - Both hunks are in `waitingLines` and the `talk` catch, away from row 171's `channelCallFor`/`callFor` hunk (sage-1022). - `transport.mjs` is untouched (channel-proof.test.mjs:13 pins it). Its EXIT map would send this code to exit 2; the CLI branch gives exit 3 instead. - `packages/cli/README.md`: the row 146 paragraph drops the tester line and points to `--tester`. The `--tester` paragraph says what the refusal prints and that it exits 3. - `docs/TOOLS.md` has no tester talk line, so it is unchanged. ## Tests (the brief's five) 1. bus tester.test.mjs, "a tester message to a role no run holds refuses tester-target-not-running and stores nothing": cto is never held. The messages, deliveries and `_bus` notice counts are unchanged. The prefix check still comes first. 2. bus tester.test.mjs, "a tester message is refused while the run is only launched, and routes once it claims": cto launched and unclaimed refuses with counts unchanged. After `role.claim`, the send returns no `waiting`, has one `routed` delivery to `cto-run`, and cto receives it. 3. bus tester.test.mjs, "a human message to a role no run holds still waits and tells launch.by": the business has a launch block (`by: pm`). The tester's send refuses; the human's send to cto is stored, returns `waiting` with `told: true`, and writes one notice to pm in reply to it. 4. cli tester.test.mjs, "talk --tester to a stopped role exits 3, prints no sent line and stores nothing": replaces the row 146 test at :130-136. Exit 3, the exact message, empty stdout, zero messages, and one tester call (`message.send`). 5. cli tester.test.mjs, "only a --tester send refused tester-target-not-running is rewritten": a stand-in tester transport refusing `tester-off`, `tester-required`, `tester-refused` and `unknown-role` keeps each error's own code and message. A human `talk` whose stand-in transport refuses `tester-target-not-running` keeps the bus error. ## Existing tests changed beyond the brief's list Both follow from the brief's change; Sage cleared them for BUILD.md in sage-1025. - `packages/bus/tests/launch-notice.test.mjs:190-192` sent a tester message to the stopped pm and asserted `waiting.told === false`. The send now refuses, so it asserts `tester-target-not-running` and an unchanged message count. The test title says "a refused one from a tester". The later tester send to the claimed coder (:197) is unchanged. - `packages/bus/tests/tester.test.mjs`, "tester-cli.mjs binds through the runtime ...": it sent through the shim to pm, which no run held in that runtime. The runtime now gets a launch record for pm, with `pid: process.pid` and `startTime: '1'`, the same shape as process.test.mjs. Since no process has that start time, the shim doesn't see this test as a managed-run ancestor. Before pm claims, a send to cto through the shim returns `{ status: 2, err: 'tester-target-not-running' }`. I checked every other tester `message.send` in packages/*/tests (messaging, names, instances-verb, staff-request, chief-of-staff, routes-verb, limits, the cli files). Each one either targets a claimed role or sends nothing. ## Mutants (run/mutants.mjs, driven by run/mutants.sh) A mutant counts as killed only when its test run exits non-zero and every title listed for it failed (`mutants.mjs expect`). - M1-bus-no-tester-check deletes the broker line. It must fail items 1, 2 and 4, and runs bus and cli tester.test.mjs. - M2-cli-no-tester-branch deletes the CLI branch. It must fail item 4, and runs cli tester.test.mjs. ## Path readers - `packages/bus/tests/project-runtime.test.mjs:13` reads broker.mjs's import lines. No import changed. - `packages/cli/tests/host.test.mjs:826` reads the cli README for the unit file name. That text is unchanged. - No test pins a sha256 of a changed path. The only 64-hex pin near these files is channel-proof.test.mjs:12, on human-cli.mjs. ## Run The run used the slot from sage-1025 and ran 18:34:29Z-18:37:12Z on 0b6ff260f. The worktree matched the manifest before and after. Scripts and output are in run/ (run.sh, mutants.sh, mutants.mjs, touched.txt, run.out). - Five files run singly (the 3 changed test files plus the 2 path readers): tests 74, pass 74, fail 0. | File | Pass | |---|---| | bus tester | 14 | | launch-notice | 8 | | cli tester | 8 | | project-runtime | 3 | | host | 41 | - Suites: bus 423/423 and cli 345/345, so tests 768, pass 768, fail 0, skipped 0. - Mutants: both controls pass. Killed 2, survived 0, timeouts 0. - M1 failed items 1, 2, 3 and 4, plus the tester-cli runtime test. - M2 failed item 4. - Not run: the scripts/test-*.sh suites. Sage's gate covers them. ## Notes - `#waiting`'s `sender !== 'tester'` term is now unreachable for a message with no holder. I kept it, since the brief changes only the comment. - The CLI and the bus run from one pinned copy, so a CLI with this change never talks to a bus without it. With the `!w.told` line gone, an older bus's `told: false` would print the "has been told" line. - Out of scope, per the brief: message 02f0e5d6 and other stored messages, expiry of messages already waiting, `thread.send`, the launcher, and the live stack. No re-pin is scheduled.