Files
jason.woltje 6bad02612a lead: commit dewey's untracked evidence (721 files: queue-124-review queue-143 queue-149 queue-165-review queue-170 queue-175 queue-177 queue-184 queue-191)
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.
2026-11-02 15:05:14 -06:00

17 KiB

Row 177 (#1675, Q173): tester threads

Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-19_tester-threads.md, blob f146cd772e (dfd3e5823, queue revs 2093-2094). Design: DESIGN.md in this packet, sha256 99ce578091bb…2660f0, approved in sage-1146 (#1675 comment 29333) with calls (a)-(c) and notes N1 and N2 folded into the build.

Base: c49de08e8 (base.txt), origin/refactor when the build started.

Round 2 candidate: e097915d95 (candidate.txt), a commit in my worktree on top of round 1's 1971683e14. Neither is on refactor. build.patch is git diff --binary c49de08e8 e097915d9 (sha256 046c6e114209…6c62c6, git patch-id --stable 15f9c7bba807…29f1a2e). candidate-manifest.sha256 lists the same 16 paths as round 1 (sha256 of the manifest 3251f94d9e7e…185854). Round 2 changes four of them: packages/bus/README.md, broker.mjs, tester-threads.test.mjs and tester-writes.test.mjs. Round 1's patch and manifest (manifest sha256 9aaabae85984…845971) are kept in r1/.

Base drift, checked at 02:51Z and again before the in-review move: refactor has moved past the base (refactor and origin/refactor are c0d6bc7d5 at 03:03Z). git diff --name-only c49de08e8 <head> -- <the 16 paths> is empty. The new commits touch only BUILD-LOG.md, docs/SESSIONS.md, the queue files, docs/plans/2026-10-20_stack-release-after-window.md, and the agents/darkwing and agents/sage packets.

What changed

  • Bus (broker.mjs).
    • TESTER_VERBS adds thread.create, thread.open, thread.send, thread.messages and assistant.stop.
    • #owner(s) is tester for a tester session and the human otherwise. It is used for the thread-not-owned check, the owner thread.create stores, the thread.list filter, the thread.open by and the assistant.stop holder scope.
    • A tester thread.send stores from_role 'tester' and from_run 'tester-cli', writes no human.input, and returns request: null. Its lists check reads the sender as the human.
    • In a tester-owned thread, a run's message.send to tester is checked against the lists as if it were to human. Its message.send to human refuses tester-thread-human.
    • A tester assistant.stop revokes only the runs of tester threads. The claim row records by 'tester' and the reason stopped by the tester.
    • The boot check refuses a business whose human is tester (invalid-business), call (a).
  • Webui.
    • The tester listener (7333) accepts chat. Its fence admits exactly the four chat write routes (TESTER_CHAT_WRITE).
    • chatHTTP gets a tester branch for writes: no sign-in, the Origin must be the listener's own, Fetch Metadata still same-origin. STATUS maps tester-off, tester-required and tester-refused to 403.
    • The webui CLI builds the tester listener's chat from the TESTER_CLI caller. 7332's chat is unchanged.
    • bus.js renders the chat with canSend || isTester.
  • CLI. Comments only: the CLI's thread verbs stay the human's, and a tester talk --thread is the follow-up row from call (b).
  • READMEs.
    • bus: the row 177 paragraph covers the verbs, owner, list rule, boot refusal, from_run, the lists rule, tester-thread-human, N1 (the chief reach is not new reach), the name leak, call (c), and where a tester reads replies (sage-1148, deviation 6).
    • webui: the tester section allows chat, keeps 7332's chat the human's, and states N2 (a loopback caller can start assistant turns; bus.tester is the switch and off answers 403 tester-off).

Deviations from the design

  1. W1-W3 and the browser test use their own business, not the shared tester fixture. The shared fixture has no assistant role, and other tests count its roles and instances. tester-fixture.mjs gains a testerChat export: a plain Broker with an assistant and a chief, bus.tester on or off, and createChat over a caller that binds the tester at each call. Only plugins is stubbed (channel on), after the bind, so bus.tester off still refuses first.

  2. tester-cli turns the channel on first. The fixture business has no plugins.channel, so the test calls the tester's plugins.set to add ["cli","webui"]. The fixture also has no assistant instance, so POST /api/chat/threads ends in the bus's thread-instance-missing. That refusal shows the call went through TESTER_CLI to thread.create.

  3. M5 is split. M5a puts the tester thread.send lists check back on the sender. M5b removes the reply substitution.

  4. chat.mjs is unchanged, as designed, but two of its strings are now stale. The header says "the current human transport", and the TypeError says "chat needs a human transport". The tester listener now passes the tester transport. Wording follow-up, no behavior change.

  5. The browser test clicks Refresh before it looks for the sent text. After a send the client shows "Accepted message …", and the log picks the message up on the next read, the same as on the human Console (chat-browser.test.mjs waits only for the receipt). The test waits for the receipt, clicks #refresh, then waits for the text in .chat-log. My first draft waited for the text without a read, and its control failed on that wait. The rerun is under Runs.

  6. The bus README says where a tester reads replies (sage-1148, #1673 comment 29334). That is message.receive (what talk --tester and follow --tester use) or thread.messages in its own thread, never inbox: inbox --tester --json prints decisions only (cli.mjs:298-300), and the messages view reads to_role 'human' for every session (broker.mjs case 'messages'). T4 reads the reply both ways (tester-threads.test.mjs:134, :137), and W1 reads through the chat's thread.messages. No code change.

Runs

All on my worktree (~/dewey-scratch/b177), after Darkwing's "clear" at 02:18Z. Every run took the slot after a passing busy check, chained with &&. DOCKER_HOST pointed at a missing socket and MOSAIC_CONFIG at a missing file, so test-task skipped its Docker cases (adapter seam, workspace/capability, live task, live recall). Sage's gate runs them. Scripts and logs are in r1/.

Mutants (r1/b177-mutants.sh, 02:18-02:19Z, r1/mut/). Each mutant is one exact-once edit with the file's sha256 checked on restore, and every mutant exited 1:

Mutant Edit Killed by
M1 #thread compares s.human T1, T2, T3 (both), T4, T7
M2 thread.list owner s.tester ? b.human : s.human T1 and the row 175 tester-reads list test
M3 from_run 'human-cli' T2, T7
M4 tester send writes human.input T2
M5a send lists check on the sender T2, T3, T4, T7
M5b no reply substitution T4
M6 no tester-thread-human guard T4
M7 stop holder scope s.human T5
M8 no boot check T6
M9 main listener's x-mosaic-tester refusal removed main-listener test, W4
M10 chat-http tester Origin check removed W2
M11 tester chat on the human busCaller CLI tester listener (tester-cli.test.mjs:71)
M12 tester write fence off "refuses every human-only write"
M13 composer canSend only the browser test, at the composer wait (tester-browser.test.mjs:35)
M14 thread.open by s.human T2

Controls on the real source: tester-threads 9/9, tester-reads 6/6, tester-listener 10/10, tester-cli 2/2, tester-writes 8/8. tester-browser was 1/2: the first draft of the row 177 browser test failed its wait for the sent text (deviation 5). I fixed the test, and the rerun's control passed 2/2. M13 was rerun against the fixed test (r1/rerun-mut/): it exits 1 at the composer wait, line 35. The first M13 result in r1/mut/ was taken against the broken test, so it doesn't count.

Suites, first run (r1/b177-gate.sh, ended 02:26:14Z, r1/suites/):

  • bus 445/445, webui 257/258 with 1 skipped, conversation 182/182.
  • The webui skip is the editor rebuild check. It needs esbuild 0.28.2, and this checkout has 0.28.1. It isn't one of my files.
  • cli failed 24/396. Every failure traces to my TMPDIR (~/dewey-scratch/tmp/b177-suites). It pushed launch socket paths past session.mjs:119's 107-byte limit ("data root path too long for a socket"). The timeouts, launch-failed and the T42 rollback-set refusal follow from it.
  • All 10 scripts passed, 340 checks in total: auth 15, conductor 17, config 24, discord 78, extension-package 18, foundation 44, land-work 86, queue 28 (plus its 156 node tests), release 4, task 26.

Rerun (r1/b177-rerun.sh, TMPDIR ~/dewey-scratch/t7, ended 02:30:28Z, r1/rerun/). This ran on the final tree, with the sage-1148 README sentence:

  • cli 396/396, T42 included.
  • tester-browser control 2/2.
  • M13 killed, as above.

The other suites ran before I added that README sentence. That is a one-sentence change to packages/bus/README.md, and no test reads that file (grep below).

Node totals by awk sum: bus, webui, conversation and the cli rerun give 1281 tests, 1280 pass, 0 fail, 1 skipped.

channel-proof: the test file is unchanged, and it passed in the 445/445 bus run. It pins four files: bus human-cli.mjs and human.mjs, cli transport.mjs and webui src/bus.mjs. None is in the manifest.

Hash-pin and path-read grep over packages//tests and scripts/test-.sh, for every path in the manifest:

  • No 64-hex pin names any of them.
  • project-runtime reads the broker's imports, which are unchanged.
  • bus-browser reads the webui README phrase "the loader's", which is still present.
  • stack-release T39 reads --tester-port in webui cli.mjs, which is still present.
  • shell.test counts fetch and method calls in bus.js, and the counts are unchanged.
  • No test reads packages/bus/README.md.

After every run, a /proc cwd sweep of ~/dewey-scratch found no processes left.

Round 2 (sage-1160, after Filbert's changes verdict, #1675 comment 29347)

Line numbers below are the round 2 candidate's packages/bus/src/broker.mjs.

F1: a tester-thread run's decision.raise to the human

  • #testerThread(s) (after #sender) is the one test: not the human, not the tester, a thread claim, and that thread's owner is tester. message.send uses it now (it was inline in round 1).
  • decision.raise (:1676) refuses tester-thread-human when route comes out human and #testerThread(s) holds. The check comes right after route is computed, so it runs before the messaging check, the supersede withdrawal (:1686, a decision_events insert) and the decision insert (:1696). Nothing is stored.
  • route is human for an uncited gated raise, a hard wait, and an arbitrated raise whose arbiter is the raising role (:1673). All three refuse. A raise routed to an arbiter that is another role stays allowed: that is the chief's reach to the human that N1 already names, not new reach.
  • Tests, tester-threads.test.mjs:
    • T8a. From the tester-thread run, a gated task.create (outside the assistant's grants) refuses tester-thread-human. The human's inbox stays [] and the decisions table has no row. The same raise from a human-thread run stores route_to 'human' and shows in the human's inbox.
    • T8b. With task.create in the assistant's crossRole, the tester-thread run's raise is stored with route_to 'chief', and the human's inbox stays [].
  • M15 removes the :1676 guard (results under the round 2 runs).
  • bus README (:614): the run's decision.raise refuses the same way when the route comes out human, so neither its replies nor its decisions reach the human's inbox. A raise routed to the chief as arbiter stays allowed.

Every broker path that stores a message or decision with a human target

The broker has one decision insert and eight message inserts, counting #reviewMessage once (the three verbs that call it are listed separately). The guards rest on three facts:

  • No role is named human or tester (:248, invalid-business).
  • Both arbiters must be roles (:256).
  • launch.by must be a declared role (packages/business/src/business.mjs:123).
Line Path Target Can a tester-thread run store a human target here?
:1696 decision.raise route_to = route No, since this round: :1676 refuses route === 'human' first.
:1894 message.send to_role = to No: :1859 refuses to === 'human' (round 1). Its to 'tester' is checked as human on the lists, but it is stored as tester.
:999 #landMessage (landing outcome, from the trusted host) s.role, the landing run's role No: the target is a role, and no role is named human.
:1806 decision.resolve / withdraw, message to the raiser d.raised_by_role No: only an agent raises (#agent(s) in decision.raise), so the raiser is a role.
:2377 thread.send t.instance No: only the human or the tester calls it (if (!s.tester) this.#human(s)), and the target is the thread's instance, a role.
:2518 via :1987 staff.request b.launch.by No: launch.by is a declared role (business.mjs:123).
:2518 via :2032 review.request a.to No. Sage's check: a.to must be a key of b.roles (:2005, unknown-role; Sage cited :1985 on the base). Its gated form needs an approved decision first, and with F1 a tester-thread run can't raise one to the human.
:2518 via :2071 review.verdict r.from, the review.requested event's actor_role No: the requester ran review.request, so r.from is a role.
:2565 #notice to launch.by by No: a role (business.mjs:123).
:2596 #specNotice (spec save) pm and cto roles No: called only from project.file.save (:1450), which is human-only (:1427-1428).

messages reads to_role 'human' and inbox reads route_to 'human', so with these guards nothing from a tester-thread run shows in either human view.

Deviation 7: DESIGN.md:82-84 was wrong

The approved design said a tester-thread run reaches the human's decisions only through a crossRole raise to the chief, and needed no guard. That is false. An uncited gated raise routes straight to human (:1672), and the assistant's grants make any action outside them gated. Filbert's probe stored one (F1). The fix is the :1676 guard. DESIGN.md stays as approved (sha256 99ce5780…2660f0); this entry records the correction.

F2 and F3

  • F2: the dispatch comment above the tester request view now names the row 175 writes (name.set, project.create) and the five row 177 verbs. It says that none of their branches checks reader, that each passes a tester by s.tester, and that each write records tester. thread.messages writes nothing.
  • F3: tester-writes.test.mjs:2 adds "Row 177 (#1675) adds the thread verbs, tested in tester-threads.test.mjs", and the :60 test name reads "each of the six reads".

Follow-ups (Sage files them after landing)

  • N1: #threadFor follows the in_reply_to chain with no owner check.
  • N2: the stale chat.mjs strings (deviation 4).
  • The tester talk --thread row from call (b).

Round 2 runs

Slot granted in sage-1162 at 02:53:22Z. My busy check said CLEAR at 02:53:32Z, chained with &&, and the run ended at 03:01:13Z. It ran from the worktree at 1971683e14 with the four round 2 files uncommitted (dirty=4 in r2/summary.txt). I committed exactly that tree as e097915d9 afterwards: git status showed no other change under packages/. TMPDIR was ~/dewey-scratch/t7 (30 bytes) for the suites and ~/dewey-scratch/t8 for the mutants. Docker was off, so test-task skipped its Docker cases. Scripts and logs are in r2/ (b177-r2.sh, b177-mutants.sh with M15 added, mut/).

  • Controls on the real source: tester-threads 11/11 (both T8 tests), tester-reads 6/6, tester-listener 10/10, tester-cli 2/2, tester-browser 2/2, tester-writes 8/8.
  • Mutants M1-M15: every one exits 1 with the same killers as round 1. M15 (the :1676 guard removed) is killed by T8a, "a tester-thread run never raises a decision to the human; a human-thread run still does". T8b passes under M15, as it should: that raise routes to the chief.
  • Suites: bus 447/447 (round 1's 445 plus T8a and T8b), webui 257/258 with 1 skipped (the esbuild 0.28.2 editor check, as in round 1), conversation 182/182, cli 396/396.
  • Node totals by awk sum over the four suite logs: 1283 tests, 1282 pass, 0 fail, 1 skipped.
  • The ten scripts: 340 passed, 0 failed, as in round 1. Queue's 156 node tests pass too.
  • channel-proof's four pinned files are still outside the manifest.
  • Grep of packages//tests and scripts/test-.sh for the four changed files:
    • No 64-hex pin names any of them.
    • No test reads packages/bus/README.md.
    • project-runtime reads broker.mjs's imports, and round 2 adds none.
    • tester-writes.test.mjs names tester-threads.test.mjs only in comments.
  • A /proc cwd sweep of ~/dewey-scratch after the run found only my own shell.