Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
19 KiB
Row 175 (#1673, Q173, mos-0870 N2, mos-0872): tester reads and writes for the test system
Builder: Dewey. Reviewer: Filbert. Gate owner: Sage.
Brief: docs/plans/2026-10-19_tester-read-checks.md, blob 41b6033da2 (commit 3dfaea286, queue rev
2048), which adds the scripts/mosaic section, test 12 and its mutant to blob 8973499247 (1c90c68c5).
Assignment sage-1074, plus sage-1081 for the wrapper. Round 2: sage-1094 (the rebase past row 169,
and Filbert's round 1 notes N1-N3 from comment 29264). Slot: Darkwing's clear after the row 169 mutants (22:53Z).
Round 2 base: 9e74925c2 (base.txt), the refactor tip with row 169's landing 264756a30 and row
179's 9e74925c2. Both changed docs/TOOLS.md, one of this row's 25 paths, so each moved the
candidate off base (the candidate-drift rule).
- Row 169 broke Filbert's round 1 approval of TOOLS.md's sha256. Round 2 first rebased onto
1bfc9833b, and that rebase applied cleanly. Row 169's TOOLS.md edit is in stack-release activate, pm-on and the two-phase PM start; this row's is the one-sentence agent-check exception at :277. At round 2's start, origin/refactor wase7d47c346, which lacks264756a30. Sage's1bfc9833b, pushed after, is the base that carries it. - Row 179 landed during the round 2 request (sage-1100).
git diff --stat 1bfc9833b 9e74925c2lists BUILD-LOG.md, docs/SESSIONS.md and docs/TOOLS.md; only TOOLS.md is a manifest path. The rebase onto9e74925c2applied cleanly.git range-diffshows all three commits as=, andgit patch-id --stableon the whole diff is c1cf0ed66006 both before and after. Only TOOLS.md's hash changed in the manifest.
Candidate: 989699f7c (candidate.txt), three commits on the base in Dewey's worktree, not on
refactor:
c0b06ed89, the row (round 1's 84e350f99, rebased).3067ae93e, sage-1081's wrapper line, test 12 and the bad-key asserts (round 1's 7b27ab3f9, rebased).989699f7c, N1-N3.
build.patch is git diff 9e74925c2 989699f7c. candidate-manifest.sha256 (digest 2e30f2a1e667)
lists the 25 changed or added paths with their sha256. That is the same path list as round 1. Six
hashes differ from round 1's manifest: docs/TOOLS.md (the rebases), and broker.mjs, cli README,
tester-reads.test, paths.mjs and tester-init.test (N1-N3).
The first round 2 request (comment 29275 on #1673, manifest bdc4e74f4aa6, f9e41b918 on 1bfc9833b)
is superseded. Its packet files are kept in r2a/.
Follow-up, not in this row: row 179's TOOLS.md step 2 says "Until row 175 lands, scripts/mosaic
doesn't dispatch plugins". After this row lands, the condition that matters is the window's
target copy. A copy from before the landing still needs the node <copy>/packages/cli/src/cli.mjs
form, and a later one takes mosaic plugins set. The 179 brief says the second row to land
rebases. This row changes no wording there, so the sentence should be reworded once it lands.
Round 1 (approved by Filbert, 29264): base d63cfca22, candidate 7b27ab3f9, manifest dc5c0361a310.
Its packet files are kept in r1/.
Row 164's pinned files
packages/bus/src/human.mjs, packages/bus/src/human-cli.mjs, packages/cli/src/transport.mjs
and packages/webui/src/bus.mjs are untouched (not in the diff). channel-proof.test.mjs passes on
the candidate. A grep of every packages/*/tests file and scripts/test-*.sh for a 64-hex value
finds pins on those four files only. A grep for each changed path finds one test that reads a
changed doc: packages/cli/tests/host.test.mjs:826 reads the cli README for the unit file's name,
which this row doesn't touch; it passes.
What ships
Bus (packages/bus/src/broker.mjs)
TESTER_VERBSis exported and frozen. It gainsusage,names,limits,plugins,instances,project.list,thread.list,name.setandproject.create(19 entries).- New exported, frozen
TESTER_ORGANIZATION_VERBS:limits,plugins,instances,plugins.set,instances.set,routes.set. In the organization branch the tester gate runs first, before any argument check, as the store's gate does: a tester sending any other organization verb (limits.set,organization.set,organization.revision,routes) refusestester-refused. Then the branch passes a tester where it used to refusehuman-required, andactor = s.tester ? "tester" : s.human, so W1-W3 receipts nametester. Every check the human's write runs still runs (revision, key lists, plugin and route checks, the instance writer's refusals). name.set(W4): a tester skips#classificationand#consumeAuthority, runs every name check, and the insert writesby = 'tester'.project.create(W6) andproject.list: a tester skips#human;created_byistester.thread.list: a tester skips#humanand the query bindsownerto the business human (b.human), so it lists the human's threads. Row 177 gives the tester its own.message.send(W5): the[mos test]check,tester-prefixandTESTER_PREFIXare gone. The keys, role-only target, no decision, no authority, nohuman.inputandtester-reply-requiredstay.#requestProjectFile(project.files,project.file,project.file.save): these dispatch before the store's tester gate, so a tester used to gethuman-requiredfrom#human. It now refusestester-refusedfirst, so the brief's item 9 code holds forproject.file.save. A behavior change for a tester only; the human's path is unchanged.
CLI (packages/cli/src/)
--testeron the seven reads:usage(usage.mjs),names(cli.mjs),limits(the read only),plugins,threads(thread.mjs),project list(project.mjs) andbusiness instances(instances.mjs). The flag stays inparseduntilcallFor/channelCallFor, which pick the tester transport.--testeron the write forms:plugins set(W1),business instance add|set(W2),name(W4),talk --tester(W5, any text),project create(W6) andproject init(W7). W3 has no CLI form.limitsrefuses--testeronset,resetandmigrate(exit 4, before any call).talkloses the early prefix check andTESTER_PREFIX.project init --tester(projectCommand), in order, before any disk write:testerProjectArea(context): refusestester-project-area(ProjectError, exit 2) for a repo outside<dataRoot>/projects/<business id>/.- One
agentsread through the tester transport, sotester-offandtester-requiredexit 3. initProject(context, { author: 'tester' }).project checktakes no--tester, and a duplicate--testeron init is exit 4.
- Usage strings gain
[--tester]on each form that takes it.
Wrapper (scripts/mosaic, sage-1081)
- The case line that sends verbs to
packages/cli/src/cli.mjsgainsplugins,name,names,follow,threadsandthread. Before, they fell through to the seat CLI, which refused withusage: mosaic launch ..., somosaic plugins set(W1),mosaic name(W4) andmosaic namesfailed through the wrapper.queueandbusinesskeep their own branches. scripts/mosaic is not a row 164 pin, and no test pins its hash.
Round 2 (sage-1094, Filbert's notes)
- N1:
testerProjectAreaalso refuses the area root itself,<dataRoot>/projects/<business>, withtester-project-area. A repository there would hold every other project's default repository.containsis unchanged, since projectRepo's overlap check still needs it to count equal paths. The check isrelative(area, repo) === '' || !contains(area, repo). The cli README'sproject init --testeritem says so. - N2: test 10's list gains
bus start acme --testerandbus stop --tester. Both already refused with exit 4 in parse, so this is a test-only change. The cli README's no---testerlist names them. - N3: a comment at the reader view in
request(broker.mjs, after the TESTER_VERBS gate). It says the view now also carries name.set and project.create, and that neither branch checksreader: each passes a tester bys.testerand recordstester.
Project (packages/project/src/)
initProject(context, { author }): the layout commit's author and committer areauthor, checked withcheckedIdbefore anything is made; without it, the business human as before.testerProjectArea(context)in paths.mjs:projectRepo(context)(its own checks first), then the samecontainstest projectRepo uses (hoisted to module scope), against the area. It writes nothing.
Docs
bus README (verb table rows for organization.set, organization.revision and name.set; the
instances, routes, tester and project-file paragraphs), cli README (usage block, the --tester
paragraph, a new project init --tester item, limits, instances and usage sections), plugins
README (step 5) and docs/TOOLS.md (one sentence: the --tester forms are the exception to the
agent check). grep -rn "tester-prefix\|mos test\]" packages scripts docs now finds only sample
message bodies in tests, which are valid bodies, not prefix rules.
Design choices and tradeoffs
- Both tester lists are exported so the item 13 test can pin them.
- The organization gate sits before the argument checks, so a tester's refused verb never reveals argument errors, as at the store gate.
containscounts the area root itself as inside (rel === ''), as projectRepo's own use does. In round 1 that let a tester init a repo at<dataRoot>/projects/<business>itself. Filbert's N1 flagged it, and round 2 refuses it intesterProjectAreaalone.- The project file verbs' tester code changes from
human-requiredtotester-refused, so the brief's item 9 holds forproject.file.saveand the tester's codes stay uniform. - The W7 CLI tests spawn
scripts/mosaicas its own process. The tester transport is spawnSync, so a tester-cli.mjs spawned from the test process couldn't reach a broker in that process; as a child of a child it can. The child's env dropsMOSAIC_BUS_(TESTER|CLI)_NONCE.
Tests (the brief's thirteen)
New files: packages/bus/tests/tester-writes.test.mjs (8 tests),
packages/cli/tests/tester-reads.test.mjs (6), packages/project/tests/tester-init.test.mjs (3).
- bus tester-writes "reads": the human sets a name, a project and a thread; each of the seven reads deep-equals between tester and human, and thread.list, names and project.list are non-empty.
- cli tester-reads "each read with --tester": for the seven forms, the output inside an agent
with
--testerequals the human form's; the tester transport is called and the human's isn't; without--testerexit 3 before any call. - bus tester-writes "W1-W3": plugins.set, instances.set and routes.set as the tester each log
actor
testerwith a matching transaction; the human's routes.set logsjason. Also bus instances-verb.test and routes-verb.test pass the tester's actor through. - bus tester-writes "W2 refusals": a credential-declaring definition
(
organization-credentials-out-of-scope), a protected definition (organization-definition-refused), the nameshumanandtester(invalid-request), and abus.testervar on add and on change (invalid-request); the file bytes, revision and log are unchanged after each. Control: an add with amodelvar lands with actortester. - bus tester-writes "W4" and names.test:
by = 'tester', no new events, the human'sjason;name-taken,name-is-instance(instance key,human,tester) andname-is-rolestill refuse. - bus tester.test.mjs "a tester message is from tester, needs no prefix, and writes no
human.input or authority" (updated): stored body without the prefix,
unknown-rolefor the targetshumanandtester,invalid-requestforcorrectsanddecision, no event. cli tester.test "talk --tester sends text with no prefix" checks the stored body. - bus tester-writes "W6":
created_bytesterandjason,project-existsstill refuses, the tester's project.list equals the human's. - cli tester-reads, three W7 tests through
scripts/mosaic: init makes the repo under the area, committedtester|tester, and plain init inside an agent still exits 3; a repo outside the area (bus.tester off) exits 2tester-project-areawith the target missing and the data root listing unchanged; off exits 3tester-offand each managed-run marker exits 3tester-required, with no projects directory. project tester-init coversinitProject's author option andtesterProjectAreadirectly; since round 2 its refused list includes the area root<dataRoot>/projects/acme(N1). - bus tester-writes "human-only": the 13 sync verbs refuse
tester-refused, and so do organization.set and organization.revision with a key neither takes (the gate runs before the argument checks),project.file.saveandproject.filesrejecttester-refused; the organization file bytes, the limits revision, the human's views and thread.messages are unchanged. - cli tester-reads:
--testerexits 4 with no calls and no projects directory on limits set/reset/migrate, decide, inbox --read/--read-all, thread create/open/read, talk/follow --thread, stop, launches off/on/list, bus start and stop (round 2, N2), project check and a duplicate--testeron init. - bus tester-writes:
varsnull,{},false,'true'and1each refusetester-off. - cli tester-reads "scripts/mosaic sends plugins, name, names, follow, threads and thread to
packages/cli": as instances.test.mjs:146, it spawns the wrapper with no config. Each verb, in a
form packages/cli refuses, exits 4 with packages/cli's own message, and stderr has no seat
usage (
mosaic launch <). app.test.mjs:832, which pins the seat usage, passes unchanged. - bus tester-writes pins
TESTER_VERBS(19) andTESTER_ORGANIZATION_VERBS(6) and checks both are frozen.
Existing tests changed
- bus tester.test: the message test drops the prefix and asserts the stored body.
- bus names.test: the tester's name.set lands with
by = 'tester'instead of refusing. - bus instances-verb.test: the tester leaves the refused set; its forged actor refuses
invalid-request, itsinstancesandinstances.setpass with actortester. - bus routes-verb.test: the tester's
routesrefusestester-refused; its routes.set passes. - bus limits.test: the refusal loop covers role and reader; the tester's limits.set refuses
tester-refusedand itslimitsread passes. - cli names.test: the usage pins gain
[--tester]. - cli tester.test: the prefix test becomes "talk --tester sends text with no prefix, and follow
--tester reads"; the
tester-prefixexit map line goes.
Runs
All runs are Dewey's, in Dewey's worktrees with TMPDIR under Dewey's scratch, on the heavy-run slot
(Darkwing's clear at 22:53Z, released to Sage at 23:10:15Z). Every count below is an awk sum over the
ℹ lines of the run's logs.
Round 1a, candidate 84e350f99 (manifest b9f438788558, 24 paths), 22:55:08Z-23:00:17Z
- The 21 touched test files, each run alone: 169 tests, 169 pass.
- Suites: bus 436/436, cli 369/369, plugins 111/111, project 40/40, business 149/149, webui 239 pass and 1 skip of 240. The skip is the existing editor-rebuild check, which needs esbuild 0.28.2; 0.28.1 is installed. Total: 1345 tests, 1344 pass, 0 fail, 1 skip.
- Mutants: 25 run in a second worktree that matches the manifest, after a control pass of every
named test file. 24 killed. m2b (organization.set let through for the tester) survived: it is a
role verb, so
#checkAuthority→#agentrefuses a testertester-refusedanyway, and the test sent valid arguments. Round 1b adds bad-key asserts that kill it.
Round 1b, candidate 7b27ab3f9 (manifest dc5c0361a310, 25 paths), 23:04:15Z-23:10:04Z
- The 21 touched files, plus app.test.mjs, stack-copy.test.mjs and stack-release.test.mjs (the other tests that spawn or copy scripts/mosaic), each run alone: 285 tests, 285 pass.
- The cli suite: 370/370 (369 plus test 12).
- Mutants after a control pass: m2b, now killed by tester-writes. m10a (drop
pluginsfrom the wrapper's case line) and m10b (dropname) are both killed by tester-reads test 12. Each file was restored and its hash checked, and the wrapper kept its mode. - Both worktrees matched the manifest at the start and at the end. A sweep after the run found no process with a working directory in Dewey's run dirs.
After the slot release, a grep for scripts/mosaic in every packages/*/tests file and
scripts/test-*.sh found more readers than the cli files above. The queue suite's
dispatch.test.mjs runs the real wrapper against fake CLIs: run alone, 4/4. None of its cases uses
the six verbs. bash -n scripts/mosaic passes and the file is still executable, which is what
test-queue.sh checks. The webui, runs and other hits mention the path only as text: unit file
lines, a usage string, or scripts/mosaic-task.mjs/mosaic-config.mjs.
The bus, plugins, project, business and webui suites weren't rerun in 1b. Their source is unchanged between 84e350f99 and 7b27ab3f9; the one bus file 1b changes is tester-writes.test.mjs, which ran alone (8/8).
Round 2, candidate f9e41b918 (manifest bdc4e74f4aa6, 25 paths), 23:33:12Z-23:41:13Z
The slot came from sage-1094. The base is 1bfc9833b. At the start origin/refactor was e7d47c346,
which didn't have row 169's landing 264756a30, so I based the round on the local commit Sage said
they would push. When the runs ended, origin/refactor was ce5807989, which contains 1bfc9833b and
changes only BUILD-LOG.md and docs/SESSIONS.md. Neither is a manifest path.
- Singles: the 21 touched files, plus app, stack-copy, stack-release and queue dispatch.test, each run alone. 309 tests, 309 pass. stack-release.test.mjs (93/93) covers row 169's change, which came in with the base.
- Suites: cli 390/390 (370 plus row 169's stack-release tests) and project 40/40.
- Mutants: all 28 rerun after a control pass of every named file, and all 28 were killed. m11
restores
rel === ''intesterProjectArea(if (!contains(area, repo))), and tester-init.test.mjs kills it. m2b is still killed by the bad-key asserts. Each file was restored and its hash checked, and the wrapper kept its mode. - Both worktrees matched the manifest at the start and at the end. A sweep after the run found no process with a working directory in Dewey's run dirs.
The bus suite wasn't rerun. Round 2 changes broker.mjs by two comment lines only (N3), and the base move changed no bus file. tester-writes, tester and the other touched bus tests ran alone.
After the rebase onto 9e74925c2 (candidate 989699f7c, manifest 2e30f2a1e667), I ran
stack-release.test.mjs alone at 23:44:37Z-23:47:40Z, as sage-1100 asks: 93/93. The runs above stand
for this candidate. Row 179 changed only docs/TOOLS.md among the manifest paths, and no test reads
TOOLS.md. Both the busy check and the manifest check passed before and after, and a sweep found no
process left.
Greps, as sage-1094 asks:
- I grepped every
packages/*/testsfile andscripts/test-*.shfor each of the 25 manifest paths. No test names paths.mjs, which the tests reach throughpackages/project/src/index.mjs. The only test that reads a changed doc is host.test.mjs:826, on the cli README, and it ran alone (41/41). The other README hits are text. No test reads docs/TOOLS.md; its one mention, stack-release.test.mjs:428, is a refusal string. - The 64-hex pins sit in 11 files. Six are bus migration tests that pin DDL, and four are discord, queue and webui fixtures. The last is channel-proof, which pins the four row 164 files. None of them is a manifest path. channel-proof.test.mjs passed alone (2/2), so those four files are unchanged.