Compare commits

...
Author SHA1 Message Date
jason.woltje f268f4de49 row224: record whole-suite passes and killed retry mutant 2026-10-24 13:43:47 -05:00
jason.woltje e4c0c8931e row224: supply required reference in creative tool fixture 2026-10-24 13:41:58 -05:00
jason.woltje 63827cc0bd row224: record CLI and bus passes and harness fixture red 2026-10-24 13:40:57 -05:00
jason.woltje 8f1b045f40 row224: await citation hydration in new launch test 2026-10-24 13:39:55 -05:00
jason.woltje b98b9e64a0 row224: record new CLI test failure and held sequence 2026-10-24 13:37:44 -05:00
jason.woltje 6160b49810 row224: supply chief Gitea reference in new launch fixture 2026-10-24 13:31:19 -05:00
jason.woltje 5c5886d2d8 row224: record local install and single CLI run failure 2026-10-24 13:21:02 -05:00
jason.woltje ed985c0442 row224: trim whitespace in plain-text failure receipt 2026-10-24 13:15:54 -05:00
jason.woltje 532938beeb row224: fix new fixture setup and record old-test stop 2026-10-24 13:15:45 -05:00
jason.woltje a1a7357457 row224: record row266 rebase and whole-file gate list (unrun) 2026-10-24 13:02:16 -05:00
jason.woltje c509dcb6ee row224: record static rebase and pending gate (unrun) 2026-10-24 13:02:00 -05:00
jason.woltje 82d6cc62c6 row224: pace failed inline sync attempts (unrun) 2026-10-24 13:02:00 -05:00
jason.woltje 212aef6161 row224: draft broker library lookups and typed tools (unrun) 2026-10-24 13:02:00 -05:00
jason.woltje 3e5041b43b Row 266: record owner review move and missing request credential 2026-10-24 12:11:42 -05:00
jason.woltje b5fb6280df Row 266: record full gate and sixteen isolated mutant kills 2026-10-24 12:11:42 -05:00
jason.woltje e1092e673e Row 266: assert allowed room plugin dispatch does not reject 2026-10-24 12:11:42 -05:00
jason.woltje abc11152a0 Row 266: add Console room timeline and member completion 2026-10-24 12:11:42 -05:00
jason.woltje 73692e1bb6 morgana: row 257 slot result on 1261fa2d8 (sage-4159): three files whole 15/29/1 pass, supervisor suite 45/45, mutants M1-M8 all killed by assertion, tree restored; BUILD.md and evidence only 2026-10-24 12:02:34 -05:00
jason.woltje fc5f282902 morgana: row 257 r2 (filbert r1 N1, N2): Object.hasOwn unknown-key refusal with inherited-name cases; critical memory edge on a 20 GiB limit (M8) 2026-10-24 12:02:34 -05:00
jason.woltje 045dd758a3 morgana: row 257 BUILD.md records sage-2202 rulings (PSI latch on first seen, PSI count across resources) 2026-10-24 12:02:34 -05:00
jason.woltje 2d7ebe012d morgana: row 257 build (Q198 S1, #1762): @mosaic/supervisor pressure parsers and pure D4 rules evaluator, tests and mutant plan; tests wait for Sage's slot 2026-10-24 12:02:34 -05:00
97 changed files with 23698 additions and 19 deletions

No files matched your search

+6
View File
@@ -5554,3 +5554,9 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r
2026-10-24T15:55:22Z | Sage | Row 225 (#1726, C2-10b, Fethry on Codex, Dewey) LANDED 9b6345466. Before: refactor at c63801959. After: Fethry's r2 candidate c1f9dfd3c (11 commits on 963303403) cherry-picked onto c63801959. 8 commits are equal by range-diff; the other 3 differ only in the append-only log hunks, kept both sides with 0 lines removed against either parent. All 13 manifest paths match manifest.r2.sha256 (517af41b). Dewey APPROVE c31599 rev 3960; request c31600 revs 3961-3962, posted by Sage because Fethry has no token. Sage gate GREEN 15:54:58Z with its own npm ci and MOSAIC_REQUIRE_EDITOR_BUILD=1: every package suite except queue 0 fail (webui 442, library 149, bus 981, cli 988 plus the allow-listed FED R7 skip, whose runner passed 1/1), all 12 scripts rc 0. The excluded M3 timeout attempt was not counted; the unchanged retry killed cleanly. Dewey N1-N3 non-blocking: the Discord relay must pass check once it calls renderReply; the M3 hang was in the new status test's after-hook teardown; the pre-217 residual stands under sage-3669. #1726 closed c31604. Not re-pinned.
2026-10-24T16:12:35Z | Sage | Row 287 (#1792, C2-21, Steelbeak on Codex, Boyd and Dewey) LANDED 0e935776e. Before: refactor at 9b6345466. After: Steelbeak's c2338ed58 and doc follow-up a5370b198 cherry-picked onto 9b6345466. One commit equal by range-diff, the other differs only in log hunks, kept both sides. 56 manifest paths match 3683b40a. Boyd APPROVE c31602 rev 3967, Dewey APPROVE c31603 rev 3968; request c31601 revs 3965-3966 posted by Sage. Sage gate GREEN 16:11:38Z: bus 998 (Gitea Docker F-A/F-B and X-G3, minted 4, hits 0), business 283, webui 442, cli 989 plus the allowed FED R7 skip, every other suite 0 fail, all 12 scripts rc 0; fixture credential values in 0 of 29 run files with a positive control. Rulings applied: sage-4125 (library grants mint declared scopes, chief read), mos-1590 (new-test fixture fixes are the lead's call when no production code or old test changes and every fail-closed refusal keeps its own test), sage-4139 (fixture fixes) and sage-4141 (test-land-work.sh is mode 100644, run via bash). Follow-up: F-C negative, a protected Story/** rule refuses the owner too; binds the first row that needs owner pushes to a protected branch. Dewey N1-N3 non-blocking: the block's last line "can write every repository this user can write" contradicts the chief's read grant; a collaborator entry outlives a failed step 5, which still fails closed; #publishFacts would accept a library project, unreachable today. Closes Dewey's 286 r2 N2 (T7/T9 real mint). #1792 closed c31606. Not re-pinned.
2026-10-24 | Fethry | Row266 C2-10c start (#1771), sage4154/4156. Before: Console seam/plugin only expose thread verbs; room.read omits check/last needed by225. New isolated worktree on487500a16, amended pin85e6fe2ecfc5. Plan: three fixed room calls, strict room HTTP, merged room UI and leading-@ completion, shared broker check hydration/last only under amendment1. No old tests or out-of-scope production edits; local installs/no canonical links; RED-first and serialized full webui/plugins/bus/scripts plus mutants under CLEAR. Detailed design/acceptance in agent BUILD.md.
2026-10-24 | Fethry | Row266 implementation verified before candidate commit. Six owned production paths plus four new tests, no old tests edited. RED history and fixture corrections recorded before action. Editor build, webui458/0, plugins120/0, bus1000/0 and all12 scripts exit0. Initial bus999/1 hit old T9 50ms timer race; sage4158 authorized three unchanged credential-file6/0 reruns and whole bus1000/0 rerun, all green. Raw/normalized TAP provenance retained. Candidate/mutants/review next; no landing claim.
2026-10-24 | Fethry | Row266 final verification: candidate3095d6ea5, manifest5f816042 (10 package paths, no old-test edit). All16 isolated mutants killed by assertions with restored hashes and unchanged candidate. Initial M01 uncaught plugin refusal excluded, strengthened only new doesNotReject assertion and full plugins120/0 rerun. Final cwd node/Chromium sweep empty, killpg cleanup recorded. Full webui458/0, plugins120/0, bus1000/0, all12 scripts0; earlier T9 race plus four unchanged green reruns retained. Push/review packet next; Sage gate and two reviews remain.
+742
View File
@@ -0,0 +1,742 @@
# Row 224 — C2-05b broker lookups and typed tools (#1725)
Owner: Della; reviewer: Dewey. Assignment sage-2795; scope extension sage-2804.
Base: `4f3fbc25a` (Researcher C2-05a). Branch: `della/row224`.
Brief: C2-05b in `docs/plans/2026-10-20_case2-rows.md` at `aca16404c`
and `51cc0445c`; design section 3, 5.3 M3 and 7.2; C2-05a BUILD rulings.
## Execution boundary and progress
Code/docs and local Git only while row 253 holds the gate. No Node (including
syntax checks), HTTPS Git, queue claim, test or push until Sage's CLEAR.
Tests are written first; their RED/GREEN runs are deferred, not claimed.
Queue next is deferred too: its executable uses Node. Sage assigned this row
explicitly. After 253 and 216 land, rebase under a new branch name.
- Pre-flight: query.mjs consumes `{repo,commit,mapping}`; broker cannot import
Git/filesystem library code because it is in the sandbox import closure.
Runtime injects the host lookup adapter; existing human file/status/sync stay.
- Pre-flight: resolved has no project list and a library is never a checkout.
Bundle needs an explicit library flag from launcher business configuration.
- M3 reading: exact creative base is five lookups plus send_message; retain
explicitly granted broker web_search/web_fetch for research/companion under
landed C2-08 and sage-2804. No generic spec, task, evidence or decision tools. Residual (mos-1308): those
reads lack broker action checks; their typed-tool absence is the only fence
for creative roles. M1/M2 deny bash; broker-side refusal is outside this row.
Mos confirmed this interpretation in mos-1308; design wording fix belongs to Gosalyn. Other definitions retain their current tools.
- Multi-library businesses use an optional project ID; omit it only with one
configured library. Universe is required for all five tools, including get.
- Missing library uses the brief's `library-not-configured`; argument errors
reuse `invalid-request` and C2-05a query codes. No new action or authority.
## Manifest (draft code, unrun)
- bus/src/broker.mjs: five lookup verbs, strict args, holder/business rechecks.
- bus/src/runtime.mjs: host lookup adapter, stale-before-read sync, served pin.
- harness/src/tools.mjs: five schemas; creative exact-set filtering.
- harness/src/bundle.mjs: library flag forwarded to typedTools.
- cli/src/launcher.mjs: one flag derived from trusted business projects.
- bus/tests/library-lookups.test.mjs: real query/broker/socket/runtime cases.
- harness/tests/library-tools.test.mjs: schemas, role snapshots, bundle parity.
- cli/tests/thread-launch.test.mjs: flag reaches a real launched bundle with no library checkout.
- agents/della/work/queue-224/BUILD.md: manifest, rulings and deferred verification ledger.
## Verification ledger
No test or syntax-check run yet. Acceptance requires full bus/cli/harness suites
and every scripts/test-*.sh on Sage's gate; focused files and affected CLI/
seat import-closure tests precede that only in an authorized CLEAR.
## Coordination
Huey272 reports no runtime/bundle/launcher overlap. Rocko219 changes launcher
THREAD_DEFINITIONS checks only, separate from our buildBundle flag argument.
The launcher argument is required: bundle cannot derive library presence from
checkout IDs (a library has no agent clone). No role, authority or config schema
changes; library project entries are already validated and protected by M9.
## Mount and pin impact (inspection only)
No new imports in broker.mjs or tools.mjs; no RUNNER_FILES or sandbox mount
addition. runtime.mjs is host-only and already imports library index.mjs.
Bundle forwards only a boolean, never a root/mapping/slot/credential. The
existing import-closure tests are bus/tests/project-runtime.test.mjs and
seat/tests/sandbox.test.mjs. Grep found no literal sha256 pin for runtime.mjs
or bundle.mjs; bundle tests derive current source digests through SOURCES.
Library sources remain in the existing whole-directory release copy.
## Runs to schedule only after CLEAR
Set TMPDIR=/mnt/storage/scratch/tmp. Focused new bus/harness files first,
then existing library.test.mjs, library-sync.test.mjs, project-runtime.test.mjs,
harness creative/tools/spec-tools/bundle files and affected CLI launch tests.
Follow with the required whole bus/cli/harness suites and Sage's full shell gate.
No timing claim yet: use the repository/Sage per-file limits and record every
command, outcome, elapsed time and timeout before acceptance. No focused skip
or pattern run substitutes for whole-file gate evidence.
## Read-through findings and residuals
- Library get requires universe although the older design table omitted it;
section 3 isolation rule and C2-05a's accepted API are binding.
- Query type enum matches mapping.TYPES (five values); manuscript is a distinct
lookup and not a find type. Unknown host-path/commit/business/accept fields
are refused; explicit project is restricted to this business's library list.
- Async reads recheck the live holder and current project revision. A stale
read attempts sync, then takes the new served snapshot; a returned fetch
failure/divergence keeps serving the old pin. Busy sync uses the current pin.
Unreadable state, no served commit or unsafe configuration refuse. Query
and sync never hold a SQLite transaction over await.
- Processing halt refuses new agent lookup work and rechecks before returning
async results. Existing human library file/status/sync behavior is unchanged.
- Timer sync and reads still use C2-03a's 60-second contract. No timer tuning or
resource-limit schema is introduced by this row. Q222 creative reply settings
are in C2-06; no reply guard or writing capability is implemented here.
- The generic tool socket's existing 60-second client timeout can precede the
server's 120-second Git timeout. This row does not change the generic timeout;
a timed-out lookup is read-only and retryable, never a write/outcome claim.
- All tests are authored but unrun. No RED, GREEN, syntax, runtime performance,
sandbox, broker/harness suite, shell gate, claim or push evidence exists yet.
## Candidate review checklist (read-through only)
- [x] Five verbs mapped to real C2-05a methods, all require universe.
- [x] Default sole library or explicit owned project; no caller repo/commit.
- [x] Human-only file/status/sync gates kept; lookup holder rechecked.
- [x] Async socket dispatch includes lookup verbs through LIBRARY_VERBS.
- [x] Stale/unknown fetch syncs first; results use current immutable served pin.
- [x] Six/eight creative names per mos-1308; other roles retain their tools.
- [x] Launcher -> bundle -> typedTools passes presence without a library clone.
- [x] No imports added to sandbox source closure; no mount change.
- [ ] Execute new tests, RED characterization, full files/suites after CLEAR.
- [ ] Rebase after rows 253/216 land, under a new branch name.
- [ ] Claim/push only in Sage-authorized gap; Dewey review and Sage gate.
Candidate preservation commits are explicitly unrun under sage-2795; they are
not completion, green-suite or landing claims. No dependencies installed.
Correction: the first committed-range diff --check found a blank line at EOF
in the new library-tools test, missed while it was untracked. Removed in a
follow-up local commit; the complete range must be checked again. No tests ran.
## Shared-log receipts for Sage (sage-2815)
The shared logs are unchanged from the base in this row. Sage appends these
receipts in the landing sweep; they are preserved here, not in the manifest.
For `BUILD-LOG.md`:
```text
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
```
For `docs/SESSIONS.md`:
```text
2026-10-22T16:36:30Z | Della (sage-2795 / sage-2804, Q220) | Row 224 C2-05b #1725, della/row224 base 4f3fbc25a | Read both pinned briefs and C2-05a source; preparing broker/typed tools and authorized host runtime/bundle seams. No queue command, Node, HTTPS Git, claim, test or push during Gander gate. Dewey reviewer; Rocko/Huey runtime coordination sent. Tests and integration verification deferred to CLEAR.
```
Local-only draft pins 19c0a7adf/7cf39c390 are superseded by the
log-free candidate. Saved as local branch della/row224-draft-with-logs before
rebuilding the unpublished candidate commit; no remote history changed.
## Dewey dewey-0852 and Sage sage-2819 — focused corrections
B1: inline freshness uses max(lastFetch, lastError.at), not lastFetch alone.
An error within 60 seconds counts as a recent attempt, so later lookups serve
the old served pin without another fetch. Keep inline sync: the brief requires
it and businesses without timed libraries have no sync timer. The extended
failed-fetch test makes reads at +1, +59,999 and +60,000 ms (one attempt total),
then +60,001 ms (second attempt). All reads must return the old commit.
Required mutant M-B1: remove lastError.at from recentAttempt. The repeated
reads must fail the attempted-count assertion. This mutant has NOT been run.
Residual (Sage will defer): the first inline fetch in each outage window can
still exceed the tool timeout (60 seconds, or the runner broker client’s
30 seconds). Bounding that fetch is out of scope for 224. Subsequent reads
are paced by persisted lastError.at. No claim of first-read outage latency.
N1: whole bus, cli and harness suites are required, plus the shell gate.
If the new thread-launch integration test skips due to sandboxProblem(),
name the skip; a skip is not launcher integration proof.
N2: cli/tests/thread-launch.test.mjs now depends on library/tests/helpers.mjs
mappingEntry() and library/tests/fixtures/mapping.json. Changes to that fixture
must rerun the CLI launch file.
N3: harness/tests/library-tools.test.mjs compares the type enum to mapping.mjs
TYPES, so adding a mapped type cannot silently leave the typed tool behind.
These edits are local code/docs only. No Node, RED/GREEN run, mutant, claim,
HTTPS Git or push. New local pin goes to Dewey/Mos/Sage for focused review.
## sage-4163 — row224-r2 static rebase, 2026-10-24
New isolated worktree: `/mnt/storage/scratch/della-row224-r2`.
Branch `della/row224-r2`, base refactor `487500a16f4ab5902c318509fdec6acc56a543ca`.
Only the two row-owned commits were cherry-picked, in order:
`38e322ea0` -> `327073400`; `23816cf28` -> `e138b2c55`.
No old prerequisite commits were copied into this series. Rows 216 and 217
are now upstream. Row 266 remains a landing dependency: rebase on its landing
when Sage announces it. No broker hunk touches room.read or its hydration.
One content conflict: the footer of cli/tests/thread-launch.test.mjs. Kept
both upstream C2-09a test bodies and every assertion, followed by our unchanged
C2-05b launched-bundle test. No test was removed or weakened. Range-diff shows
only that context relocation and the upstream sandbox workspace expression
beside launcher's added library flag. The B1 commit is patch-identical.
B1 answer: e138b2c55 retains max(lastFetch, lastError.at), the repeated-read
regression and M-B1 specification. Dewey already checked the original fix at
23816cf28 in dewey-0853; it still needs executed mutant and gate evidence.
N2 dependency also includes harness/tests/library-tools.test.mjs importing
library/src/mapping.mjs TYPES; changes there must rerun that harness file.
Static checks: complete-range git diff --check clean; nine manifest paths.
Scanned packages/*/tests for every changed basename alongside literal 64-hex
hashes (three lines on either side): no matching hash pins. Broker and tools
add no imports; no RUNNER_FILES or sandbox mount changes.
No Node, syntax check, test, mutant, install or build was run. The historical
verification ledger above records the original preparation, not current gate
permission. Under sage-4163 the required gate is Dewey's manifest approval,
whole bus and harness suites, plus every scripts/test-*.sh on Sage's gate;
record every skip. Tests and mutants wait for Sage's explicit CLEAR. Static
Git and pushing this new branch are authorized now. Shared logs remain
unchanged; Sage owns their append-only landing receipts.
## sage-4181 — row266 landing rebase, 2026-10-24
Rebased the complete r2 series onto refactor
`3e5041b43b36d7f1c5ba7ce2f14b3634fe0955db` under new branch
`della/row224-r3`; r2 is preserved, no force-push. No conflicting files and
no manual conflict resolutions. Range-diff: all three prior commits equal
(212aef616, 82d6cc62c, c509dcb6e). B1 remains patch-identical.
Complete-range diff --check is clean. No test, syntax or mutant run.
Own touched test files, each to run whole in Sage's slot:
- packages/bus/tests/library-lookups.test.mjs
- packages/cli/tests/thread-launch.test.mjs
- packages/harness/tests/library-tools.test.mjs
Tests changed by the new upstream base, also named for the rebase-rule gate:
- packages/bus/tests/room-check.test.mjs
- packages/plugins/tests/room-channel.test.mjs
- packages/supervisor/tests/pressure.test.mjs
- packages/supervisor/tests/purity.test.mjs
- packages/supervisor/tests/rules.test.mjs
- packages/webui/tests/room-browser.test.mjs
- packages/webui/tests/room-http.test.mjs
None of those seven upstream test files has a row224-owned diff. Full bus and
harness suites plus every scripts/test-*.sh remain the row gate; the listed
CLI and upstream files require whole-file runs under sage-4181 too. Dewey's
delta review follows Sage's run. No CLEAR is inferred from the rebase.
## sage-4192 CLEAR — run receipt and old-test stop, 2026-10-24
Candidate named by CLEAR: a1a7357457a31d219b99effa9b312ec78d06f060.
Fresh mktemp TMPDIR: /mnt/storage/scratch/tmp/della-224-4192.r3L7cj.
Each command used nice -n 19 and timeout -k 10 300 node --test FILE.
Inherited OS niceness was -4, so nice initially yielded 15; corrected the
current runner tree to absolute 19 during the CLI run. Future runs must set
absolute 19 before spawning. No other seat's processes were changed.
First whole bus/tests/library-lookups.test.mjs run: 11 tests, 8 pass, 3 fail,
0 skipped, duration 1.173 s (exit 1). All three are this row's new tests.
Root causes: Broker clones businesses, so mutating the original fixture did
not publish the second project or the delisting; runtime requires the
selected harness to be enabled. Fixed only new fixture setup: reloadBusiness
after each of the two changes and plugins.harness ['pi'] in runtime fixture.
All assertions and production code are unchanged. Whole-file rerun: 11 pass,
0 fail/cancelled/skipped, 1.322 s (exit 0). B1 regression included and passed;
M-B1 has not been executed.
Next whole cli/tests/thread-launch.test.mjs reported OLD-test reds at :167,
:216 and :241. On observing them stopped only this run's process tree; no
rerun or next file was started. Interrupted-file summary: 0 pass, 3 fail,
1 cancelled, 0 skipped, 62.988 s. Returned slot to Sage in della-0041 under
mos-1592. No old test changed. No in-review move: the third own file, all
seven new-base files, both whole suites and mutant evidence remain pending.
No shell/full landing gate was run.
Exact completed run receipts:
```text
{"name": "packages/bus/tests/library-lookups.test.mjs", "command": ["nice", "-n", "19", "timeout", "-k", "10", "300", "node", "--test", "packages/bus/tests/library-lookups.test.mjs"], "exit": 1, "seconds": 1.173, "TMPDIR": "/mnt/storage/scratch/tmp/della-224-4192.r3L7cj"}
{"name": "packages/bus/tests/library-lookups.test.mjs", "command": ["nice", "-n", "19", "timeout", "-k", "10", "300", "node", "--test", "packages/bus/tests/library-lookups.test.mjs"], "exit": 0, "seconds": 1.322, "TMPDIR": "/mnt/storage/scratch/tmp/della-224-4192.r3L7cj"}
```
Old-test failure output (interrupted whole file, not a whole-file pass):
```text
✖ REQ-THREAD-2 REQ-LAUNCH-1 REQ-MSG-1 a thread message starts the thread's session, which stops when idle; its history survives the stop and a restart and no other thread sees it (20120.78425ms)
✖ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (185.485131ms)
✖ REQ-THREAD-2 REQ-LAUNCH-1 a turn longer than the idle time isn't stopped, and the thread idles out after it (30082.743341ms)
Interrupted while running:
⚠ packages/cli/tests/thread-launch.test.mjs (packages/cli/tests/thread-launch.test.mjs:1:1)
✖ packages/cli/tests/thread-launch.test.mjs (62982.9717ms)
ℹ tests 4
ℹ suites 0
ℹ pass 0
ℹ fail 3
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 62987.534861
✖ failing tests:
test at packages/cli/tests/thread-launch.test.mjs:167:1
✖ REQ-THREAD-2 REQ-LAUNCH-1 REQ-MSG-1 a thread message starts the thread's session, which stops when idle; its history survives the stop and a restart and no other thread sees it (20120.78425ms)
Error: timed out waiting
at until (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:48:9)
at async ask (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:153:17)
at async TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:179:17)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
test at packages/cli/tests/thread-launch.test.mjs:216:1
✖ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (185.485131ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'harness-binary-missing'
- 'capacity-full'
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:227:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'harness-binary-missing',
expected: 'capacity-full',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/thread-launch.test.mjs:241:1
✖ REQ-THREAD-2 REQ-LAUNCH-1 a turn longer than the idle time isn't stopped, and the thread idles out after it (30082.743341ms)
Error: timed out waiting
at until (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:48:9)
at async TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:251:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
test at packages/cli/tests/thread-launch.test.mjs:1:1
✖ packages/cli/tests/thread-launch.test.mjs (62982.9717ms)
'Promise resolution is still pending but the event loop has already resolved'
```
Receipt correction: copied test output contained two whitespace-only lines;
complete-range diff-check caught them. Trimmed those log lines in this
follow-up; no test/code or failure text changed, and no test was rerun.
## sage-4195 / mos-1595 — pre-install evidence
No canonical node_modules link made; that suggestion was refused under
mos-1575. Before installing anything in this worktree, ran:
```text
$ ls -l node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js
ls: cannot access 'node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js': No such file or directory
rc=2
```
The file is missing, so the authorized local npm ci follows. This is an
environment finding; prior existing-test reds do not establish a row-code
regression. No old-test edit or rerun preceded this CLEAR.
## sage-4195 — install and single-run STOP receipt
```text
$ [ -L node_modules ] && rm node_modules; timeout -k 10 900 npm ci --ignore-scripts --no-audit --no-fund
npm warn deprecated [email protected]: Use your platform's native DOMException instead
added 128 packages in 2s
npm_ci_rc=0
$ readlink -f node_modules/.bin/pi
/mnt/storage/scratch/della-row224-r2/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js
```
One authorized whole-file invocation, no retry or test edit. Absolute runner
and timeout niceness 19 before launch; observed Node parent/worker niceness
16 during execution (host ananicy rule), recorded without stopping for nice.
Fresh TMPDIR and command in receipt below. Stopped test process tree upon
observing existing-test reds. Interrupted summary: 0 pass, 2 fail, 1 cancelled,
0 skipped; exit 1, 40.056 s. This is not a whole-file pass. The capacity test
now receives launch-failed, rather than harness-binary-missing. Root cause of
that launch failure is not established. No assertion/production changes.
Slot returned to Sage in della-0044 for escalation to Mos; all other files,
suites and M-B1 remain held pending their own CLEAR.
```text
{
"command": [
"timeout",
"-k",
"10",
"900",
"node",
"--test",
"packages/cli/tests/thread-launch.test.mjs"
],
"TMPDIR": "/mnt/storage/scratch/tmp/della-224-4195._yq176su",
"exit": 1,
"seconds": 40.056,
"start_runner_nice": 19,
"observed_timeout_nice": 19
}
```
Failure output:
```text
✖ REQ-THREAD-2 REQ-LAUNCH-1 REQ-MSG-1 a thread message starts the thread's session, which stops when idle; its history survives the stop and a restart and no other thread sees it (20109.08171ms)
✖ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (330.738356ms)
Interrupted while running:
⚠ packages/cli/tests/thread-launch.test.mjs (packages/cli/tests/thread-launch.test.mjs:1:1)
✖ packages/cli/tests/thread-launch.test.mjs (40023.3188ms)
ℹ tests 3
ℹ suites 0
ℹ pass 0
ℹ fail 2
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 40027.912798
✖ failing tests:
test at packages/cli/tests/thread-launch.test.mjs:167:1
✖ REQ-THREAD-2 REQ-LAUNCH-1 REQ-MSG-1 a thread message starts the thread's session, which stops when idle; its history survives the stop and a restart and no other thread sees it (20109.08171ms)
Error: timed out waiting
at until (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:48:9)
at runNextTicks (node:internal/process/task_queues:65:5)
at process.processTimers (node:internal/timers:615:9)
at async ask (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:153:17)
at async TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:179:17)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
test at packages/cli/tests/thread-launch.test.mjs:216:1
✖ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (330.738356ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'launch-failed'
- 'capacity-full'
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:227:10)
at runNextTicks (node:internal/process/task_queues:65:5)
at process.processTimers (node:internal/timers:615:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'launch-failed',
expected: 'capacity-full',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/thread-launch.test.mjs:1:1
✖ packages/cli/tests/thread-launch.test.mjs (40023.3188ms)
'Promise resolution is still pending but the event loop has already resolved'
```
## sage-4204 — chief credential fixture correction (unrun)
Sage's control pair with a 29-byte TMPDIR: base 3e5041b43 passes 14/14;
ed985c044 passes all 14 existing tests but this row's new CLI fixture refuses
chief-library-gitea-required before launch. Added only chief.credentials.gitea
as a copy of the fixture coder's reference in the C2-05b edit callback, matching
cli/tests/launcher.test.mjs:35 and trackers-boot.test.mjs:17. This is a reference
to synthetic fixture credentials; no credential value was read or introduced.
All assertions, old tests and production code unchanged. No run performed.
The earlier 48-byte TMPDIR explains the launch-failed reds through the
107-byte tools.sock limit, as confirmed by Sage's control. Future named CLEAR
uses shell TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXX), 29 bytes with
alphanumeric suffix, not Python mkdtemp or a canonical node_modules link.
Next authorized run order: whole thread-launch first, other eight files, whole
bus/harness suites, M-B1; all await next CLEAR after Fethry/Gyro slots.
## sage-4208 — first-file red, sequence stopped
Named candidate 6160b4981. Whole thread-launch.test.mjs invoked once.
Shell-created TMPDIR /mnt/storage/scratch/tmp/gCXV, 29 bytes; absolute runner
and timeout niceness 19 at start, Node observed 16 afterwards (host policy).
15 tests: all 14 existing tests pass, the one new C2-05b fails; no skips or
cancellations. Exit 1 in 18.740 s. Stopped sequence and returned slot to Sage
in della-0047 before any edit or rerun. No other file, suite or mutant ran.
The original r3 list has nine files beyond thread-launch (two row-owned plus
seven upstream), rather than eight; none started in this CLEAR.
New-test failure is call(...).find is not a function in existing ask helper
:153, called by new C2-05b :550. Root cause not yet established; no assertion,
fixture or production change made. Await Sage's own-new-test ruling.
Exact run receipt:
```text
{"number": 1, "name": "packages/cli/tests/thread-launch.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/cli/tests/thread-launch.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gCXV", "start_nice": 19, "observed_timeout_nice": 19, "exit": 1, "seconds": 18.74}
```
Failure output:
```text
ℹ tests 15
ℹ suites 0
ℹ pass 14
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 18707.936662
✖ failing tests:
test at packages/cli/tests/thread-launch.test.mjs:540:1
✖ C2-05b a real launched bundle gets lookups for a library project without a library checkout (139.729167ms)
TypeError: call(...).find is not a function
at file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:153:71
at until (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:44:21)
at ask (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:153:23)
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/cli/tests/thread-launch.test.mjs:550:9)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
```
## sage-4210 — new-test async citation fixture fix
Only C2-05b now inlines send/wait, awaits thread.messages before .find, and
keeps the from_role assistant / to_role human assertions previously provided
by ask. Comment names broker.mjs:4854 async citation hydration. Existing ask,
old tests and production unchanged. This is Sage's own-new-test ruling under
mos-1590; Dewey re-reviews the delta. Whole-file rerun once authorized, then
the nine other files, whole bus/harness, M-B1 and clean-tree check on green.
Each gets a fresh shell gXXX TMPDIR and absolute nice 19; any red stops.
## sage-4210 — resumed sequence, harness new-test red
On 8f1b045f4: whole CLI 15/15 pass, no skips (19.164 s); whole bus lookup
11/11 pass, no skips (1.309 s). Own new harness file 1 pass, 3 fail, no skips
(0.129 s); stopped sequence, returned slot in della-0050 before edits/reruns.
Each invocation used fresh shell gXXX TMPDIR, 29 bytes. Runner was set to
absolute nice 19 before first spawn; host subsequently reniced runner too,
so timeout process observations were 19, -4, -4. Correct future driver to set
absolute 19 in each child before exec, as well as parent. Priority-only change
did not stop these runs. No claim that later timeout processes retained 19.
All three new harness failures are fixture validation: roles.bean.credentials
must reference gitea (got none). Shared role() helper in this row creates bean
without its required credential reference. No fixture/assertion/production edit
or rerun yet; awaiting Sage's ruling. Seven upstream files, whole bus/harness
suites and M-B1 not run. No full landing gate or in-review move.
Exact receipts:
```text
{"number": 1, "name": "packages/cli/tests/thread-launch.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/cli/tests/thread-launch.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gBj5", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 19.164}
{"number": 2, "name": "packages/bus/tests/library-lookups.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/bus/tests/library-lookups.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gD8X", "start_nice": 19, "observed_timeout_nice": -4, "exit": 0, "seconds": 1.309}
{"number": 3, "name": "packages/harness/tests/library-tools.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/harness/tests/library-tools.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gt7o", "start_nice": 19, "observed_timeout_nice": -4, "exit": 1, "seconds": 0.129}
```
Harness failure output:
```text
✖ M3: per-role snapshots contain six names, or eight with the C2-08 web grant (6.427541ms)
✔ every noncreative role gets lookups when a library is configured, not otherwise (6.837154ms)
✖ lookup schemas require a universe even for get, include paging, and cannot select repo or commit (1.52147ms)
✖ bundle flag reaches both harness tool manifests and creative prompt without generic tools (2.019053ms)
ℹ tests 4
ℹ suites 0
ℹ pass 1
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 99.111266
✖ failing tests:
test at packages/harness/tests/library-tools.test.mjs:19:1
✖ M3: per-role snapshots contain six names, or eight with the C2-08 web grant (6.427541ms)
Error [BusinessError]: /mnt/storage/scratch/tmp/gt7o/mosaic-harness-biz-CZEENl/businesses/acme.json roles.bean.credentials must reference exactly the services its role definition needs (gitea; got none)
at refuse (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/errors.mjs:12:9)
at checkCredentialMap (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:51:5)
at checkRoles (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:162:20)
at validateBusinessDocument (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:251:34)
at resolvedFor (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/helpers.mjs:46:20)
at role (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:13:49)
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:21:42)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17) {
exitCode: 2
}
test at packages/harness/tests/library-tools.test.mjs:38:1
✖ lookup schemas require a universe even for get, include paging, and cannot select repo or commit (1.52147ms)
Error [BusinessError]: /mnt/storage/scratch/tmp/gt7o/mosaic-harness-biz-iAwXhO/businesses/acme.json roles.bean.credentials must reference exactly the services its role definition needs (gitea; got none)
at refuse (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/errors.mjs:12:9)
at checkCredentialMap (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:51:5)
at checkRoles (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:162:20)
at validateBusinessDocument (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:251:34)
at resolvedFor (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/helpers.mjs:46:20)
at role (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:13:49)
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:39:40)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18) {
exitCode: 2
}
test at packages/harness/tests/library-tools.test.mjs:57:1
✖ bundle flag reaches both harness tool manifests and creative prompt without generic tools (2.019053ms)
Error [BusinessError]: /mnt/storage/scratch/tmp/gt7o/mosaic-harness-biz-c2N3Bv/businesses/acme.json roles.bean.credentials must reference exactly the services its role definition needs (gitea; got none)
at refuse (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/errors.mjs:12:9)
at checkCredentialMap (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:51:5)
at checkRoles (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:162:20)
at validateBusinessDocument (file:///mnt/storage/scratch/della-row224-r2/packages/business/src/business.mjs:251:34)
at resolvedFor (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/helpers.mjs:46:20)
at role (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:13:49)
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/harness/tests/library-tools.test.mjs:60:65)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18) {
exitCode: 2
}
```
## sage-4213 — creative fixture credential reference
Only the new library-tools.test.mjs role() helper now gives bean its required
gitea reference copied from coder, matching creative.test.mjs:27. No assertion,
shared helper, old test or production change. One whole harness-file retry
then seven upstream files, whole bus/harness, M-B1 and clean-tree check are
cleared; no CLI or lookup rerun needed (both passed, unchanged). Each child
sets absolute niceness 19 before exec, with fresh shell gXXX TMPDIR.
## sage-4213 — GREEN verification and M-B1 receipt
Verified candidate e4c0c8931; prior CLI/lookup passes on 8f1b045f4 remain
applicable under sage-4213 (only harness fixture and receipts changed).
Every run below used a fresh shell mktemp -d /mnt/storage/scratch/tmp/gXXX,
29 bytes. Each child set absolute niceness 19 immediately before exec; timeout
process observations were 19 for all ten runs and M-B1. Bus-suite Node later
observed at nice 16, consistent with host ananicy policy; no stop for nice.
No unexpected red, cancellation or skip in this cleared sequence.
Whole-file results:
| File or suite | Pass | Fail | Skip | Seconds |
| --- | ---: | ---: | ---: | ---: |
| packages/harness/tests/library-tools.test.mjs | 4 | 0 | 0 | 0.213 |
| packages/bus/tests/room-check.test.mjs | 2 | 0 | 0 | 0.21 |
| packages/plugins/tests/room-channel.test.mjs | 1 | 0 | 0 | 0.102 |
| packages/supervisor/tests/pressure.test.mjs | 15 | 0 | 0 | 0.101 |
| packages/supervisor/tests/purity.test.mjs | 1 | 0 | 0 | 0.102 |
| packages/supervisor/tests/rules.test.mjs | 29 | 0 | 0 | 0.102 |
| packages/webui/tests/room-browser.test.mjs | 9 | 0 | 0 | 6.801 |
| packages/webui/tests/room-http.test.mjs | 7 | 0 | 0 | 0.183 |
| bus-whole | 1011 | 0 | 0 | 30.46 |
| harness-whole | 176 | 0 | 0 | 13.346 |
Earlier cleared whole CLI 15/15 (19.164 s), bus lookup 11/11 (1.309 s),
zero skips. Whole bus includes lookup again; whole harness includes fixture
file again. No full shell/landing gate run: every scripts/test-*.sh belongs
to Sage's landing gate, not this CLEAR.
M-B1: temporarily replaced only lastError.at freshness term with -Infinity.
Whole lookup file: 10 pass, 1 expected fail, no skips; killed at :178 attempted
assertion (actual 2, expected 1, recent lastError prevents another sync).
Restored exact original runtime bytes in finally. Afterwards git diff --stat
and git status --porcelain both empty against committed e4c0c8931, before
adding this receipt. Compared with named 6160b4981 the only later package
changes are Sage-authorized async CLI fixture and harness credential fixture;
no mutant or production change remains. No extra post-restore rerun.
Exact commands, TMPDIRs, exit codes and elapsed receipts:
```text
{"number": 1, "name": "packages/harness/tests/library-tools.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/harness/tests/library-tools.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gct3", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.213}
{"number": 2, "name": "packages/bus/tests/room-check.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/bus/tests/room-check.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/g0ki", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.21}
{"number": 3, "name": "packages/plugins/tests/room-channel.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/plugins/tests/room-channel.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gyTq", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.102}
{"number": 4, "name": "packages/supervisor/tests/pressure.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/supervisor/tests/pressure.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gK1n", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.101}
{"number": 5, "name": "packages/supervisor/tests/purity.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/supervisor/tests/purity.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gFne", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.102}
{"number": 6, "name": "packages/supervisor/tests/rules.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/supervisor/tests/rules.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gs9Y", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.102}
{"number": 7, "name": "packages/webui/tests/room-browser.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/webui/tests/room-browser.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gP9P", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 6.801}
{"number": 8, "name": "packages/webui/tests/room-http.test.mjs", "command": ["timeout", "-k", "10", "900", "node", "--test", "packages/webui/tests/room-http.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/gP9G", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 0.183}
{"number": 9, "name": "bus-whole", "command": ["timeout", "-k", "10", "1800", "node", "--test", "packages/bus/tests/access-revoke.test.mjs", "packages/bus/tests/account-migration.test.mjs", "packages/bus/tests/address.test.mjs", "packages/bus/tests/agents.test.mjs", "packages/bus/tests/approval-binding.test.mjs", "packages/bus/tests/broker.test.mjs", "packages/bus/tests/business.test.mjs", "packages/bus/tests/channel-proof.test.mjs", "packages/bus/tests/chief-of-staff.test.mjs", "packages/bus/tests/citation-availability.test.mjs", "packages/bus/tests/citation-guard.test.mjs", "packages/bus/tests/citation-runtime.test.mjs", "packages/bus/tests/comment-cursors.test.mjs", "packages/bus/tests/comment-migration.test.mjs", "packages/bus/tests/console-decide.test.mjs", "packages/bus/tests/creative.test.mjs", "packages/bus/tests/credential-claim.test.mjs", "packages/bus/tests/credential-status.test.mjs", "packages/bus/tests/credential-store.test.mjs", "packages/bus/tests/credential-verbs.test.mjs", "packages/bus/tests/credentials.test.mjs", "packages/bus/tests/csuite-messaging.test.mjs", "packages/bus/tests/daily-quota-runtime.test.mjs", "packages/bus/tests/decision-close.test.mjs", "packages/bus/tests/decision-route.test.mjs", "packages/bus/tests/decisions.test.mjs", "packages/bus/tests/end-launch.test.mjs", "packages/bus/tests/fire-revokes-grants.test.mjs", "packages/bus/tests/gitea-grant-docker.test.mjs", "packages/bus/tests/gitea-grant.test.mjs", "packages/bus/tests/gitea-migration.test.mjs", "packages/bus/tests/grant-enroll.test.mjs", "packages/bus/tests/grant-executor.test.mjs", "packages/bus/tests/grant-host.test.mjs", "packages/bus/tests/grant-migration.test.mjs", "packages/bus/tests/grant-scope-runtime.test.mjs", "packages/bus/tests/grant-scope.test.mjs", "packages/bus/tests/hard-waits.test.mjs", "packages/bus/tests/host-run.test.mjs", "packages/bus/tests/host-tools.test.mjs", "packages/bus/tests/human.test.mjs", "packages/bus/tests/instances-verb.test.mjs", "packages/bus/tests/interpreter.test.mjs", "packages/bus/tests/land.test.mjs", "packages/bus/tests/launch-notice.test.mjs", "packages/bus/tests/launcher-released.test.mjs", "packages/bus/tests/library-credential.test.mjs", "packages/bus/tests/library-lookups.test.mjs", "packages/bus/tests/library-sync.test.mjs", "packages/bus/tests/library.test.mjs", "packages/bus/tests/lifecycle-revoke.test.mjs", "packages/bus/tests/lifecycle-verbs.test.mjs", "packages/bus/tests/limit-checks.test.mjs", "packages/bus/tests/limit-stop.test.mjs", "packages/bus/tests/limits-runtime.test.mjs", "packages/bus/tests/limits.test.mjs", "packages/bus/tests/message-citation.test.mjs", "packages/bus/tests/message-filter.test.mjs", "packages/bus/tests/messages.test.mjs", "packages/bus/tests/messaging.test.mjs", "packages/bus/tests/name-migration.test.mjs", "packages/bus/tests/names.test.mjs", "packages/bus/tests/peer-transport.test.mjs", "packages/bus/tests/peercred.test.mjs", "packages/bus/tests/plugins-verb.test.mjs", "packages/bus/tests/principal.test.mjs", "packages/bus/tests/process.test.mjs", "packages/bus/tests/processing-runtime.test.mjs", "packages/bus/tests/processing-timeout.test.mjs", "packages/bus/tests/processing-web.test.mjs", "packages/bus/tests/processing.test.mjs", "packages/bus/tests/project-await.test.mjs", "packages/bus/tests/project-files.test.mjs", "packages/bus/tests/project-runtime.test.mjs", "packages/bus/tests/publish.test.mjs", "packages/bus/tests/reload.test.mjs", "packages/bus/tests/replies.test.mjs", "packages/bus/tests/requirements.test.mjs", "packages/bus/tests/review.test.mjs", "packages/bus/tests/revoke-window.test.mjs", "packages/bus/tests/room-check.test.mjs", "packages/bus/tests/rooms.test.mjs", "packages/bus/tests/routes-verb.test.mjs", "packages/bus/tests/schema.test.mjs", "packages/bus/tests/session-clear.test.mjs", "packages/bus/tests/session-compactions.test.mjs", "packages/bus/tests/sessions.test.mjs", "packages/bus/tests/single-use.test.mjs", "packages/bus/tests/spec-migration.test.mjs", "packages/bus/tests/spec-review.test.mjs", "packages/bus/tests/spec-socket.test.mjs", "packages/bus/tests/spec.test.mjs", "packages/bus/tests/staff-request.test.mjs", "packages/bus/tests/state.test.mjs", "packages/bus/tests/store.test.mjs", "packages/bus/tests/task-page.test.mjs", "packages/bus/tests/task-read.test.mjs", "packages/bus/tests/tasks.test.mjs", "packages/bus/tests/tester-threads.test.mjs", "packages/bus/tests/tester-writes.test.mjs", "packages/bus/tests/tester.test.mjs", "packages/bus/tests/thread-claims.test.mjs", "packages/bus/tests/thread-migration.test.mjs", "packages/bus/tests/thread-shares.test.mjs", "packages/bus/tests/threads.test.mjs", "packages/bus/tests/transport.test.mjs", "packages/bus/tests/usage-broker.test.mjs", "packages/bus/tests/usage-migration.test.mjs", "packages/bus/tests/usage-runtime.test.mjs", "packages/bus/tests/usage.test.mjs", "packages/bus/tests/user-principal.test.mjs", "packages/bus/tests/viewer.test.mjs", "paLine truncated
{"number": 10, "name": "harness-whole", "command": ["timeout", "-k", "10", "1800", "node", "--test", "packages/harness/tests/bundle.test.mjs", "packages/harness/tests/citation-runner.test.mjs", "packages/harness/tests/claude-gate.test.mjs", "packages/harness/tests/claude-session.test.mjs", "packages/harness/tests/creative.test.mjs", "packages/harness/tests/gate.test.mjs", "packages/harness/tests/git-tools.test.mjs", "packages/harness/tests/library-tools.test.mjs", "packages/harness/tests/mcp-server.test.mjs", "packages/harness/tests/pi-session.test.mjs", "packages/harness/tests/raise-domain.test.mjs", "packages/harness/tests/runner.test.mjs", "packages/harness/tests/skills.test.mjs", "packages/harness/tests/spec-tools.test.mjs", "packages/harness/tests/tools.test.mjs"], "TMPDIR": "/mnt/storage/scratch/tmp/g8Yn", "start_nice": 19, "observed_timeout_nice": 19, "exit": 0, "seconds": 13.346}
{
"mutant": "M-B1",
"change": "drop lastError.at term from recentAttempt",
"command": [
"timeout",
"-k",
"10",
"900",
"node",
"--test",
"packages/bus/tests/library-lookups.test.mjs"
],
"TMPDIR": "/mnt/storage/scratch/tmp/g8my",
"start_nice": 19,
"observed_timeout_nice": 19,
"exit": 1,
"seconds": 1.359
}
$ git diff --stat
(empty)
$ git status --porcelain
(empty)
HEAD=e4c0c8931 (authorized fixture fix after 6160b4981)
```
M-B1 output:
```text
✔ all five agent verbs read the configured served commit through real queries (130.428728ms)
✔ each verb refuses missing universe and unknown keys before host work (78.900515ms)
✔ no library, missing host adapter and undeclared project refuse library-not-configured (149.899073ms)
✔ reads need the current holder on each call; human/tester/reader are not agents (90.636537ms)
✔ releasing a claim during an async lookup fences the response (60.938885ms)
✔ a business library removed during an async read cannot leak its result (51.783488ms)
✖ freshness boundary, unknown last fetch and failed fetch still use immutable served state (123.453393ms)
✔ an unreadable sync state fails closed; a busy sync keeps serving the existing pin (89.763849ms)
✔ a stale lookup sees the commit published by its sync, not the prior served commit (108.213675ms)
✔ real socket awaits lookup results and rechecks rejected query arguments (152.413684ms)
✔ runtime actually injects the lookup adapter, without exposing human library.file (157.701866ms)
ℹ tests 11
ℹ suites 0
ℹ pass 10
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1327.433364
✖ failing tests:
test at packages/bus/tests/library-lookups.test.mjs:153:1
✖ freshness boundary, unknown last fetch and failed fetch still use immutable served state (123.453393ms)
AssertionError [ERR_ASSERTION]: recent lastError prevents another sync even though lastFetch is null
2 !== 1
at TestContext.<anonymous> (file:///mnt/storage/scratch/della-row224-r2/packages/bus/tests/library-lookups.test.mjs:178:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
```
+144
View File
@@ -0,0 +1,144 @@
# Row266 C2-10c — Console rooms (#1771)
Fethry14f80e3c, branch fethry/row266, NEW worktree /home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266.
Original base962046aaa; fast-forwarded to doc-only487500a16 per sage4156. Both225 branches untouched.
Pinned amended brief85e6fe2ecfc5, C2-10c; prior3a5ed91a had identical section but stale whole-file pin.
Review order Boyd48a3ae38 then Dewey84fb346c; Sage owns landing. No token/other-seat credential access.
Slot CLEAR sage4154 lasts until returned. Local root/webui npm ci, no dependency links.
TMPDIR=/mnt/storage/scratch/tmp/KV9rHb, MOSAIC_REQUIRE_EDITOR_BUILD=1. Implementation and targeted checks complete; full gate in progress.
## Current code and resolved seam
- chat.mjs is the channel-gated fixed-verb seam. Add roomOpen({project}), roomRead({project,after,limit}),
roomSend({project,body}), exactly one matching room verb each. Never fetch thread.messages to hydrate rooms.
- chat-http.mjs already provides signed-in/send/Origin/Fetch Metadata, bounded JSON, strict keys, safe
error text, audit and outcome-unknown handling. Extend it with POST rooms/PROJECT/open {},
GET rooms/PROJECT/messages ?after&limit, POST same messages {body}. Four room codes map409 and fixed lines.
Open returns {project,threads}; send returns {ids,to}; read returns {rows,after,hasMore,at}.
Room cursor is the LAST id in the final entry.ids, not the first entry.id; the broker groups human sends.
- webui plugin needs exactly room.open/read/send in its allowlist, still rejecting arbitrary verbs.
- /api/chat/instances already returns threaded instance keys and persona.name labels. No serve.mjs edit.
- room.open returns canonical member threads; room.read returns one merged human entry with ids/to.
Read-only GET must never open/create a room or wake an agent. No implicit retry of any write.
- Amendment1 (sage4156): broker.mjs is owned only for shared check-event hydration and routing room.read
through #withLast under the same condition as thread.messages. The missing check would otherwise
hide badges AND reopen225's forged-notice defect. No other broker verb/refusal/field changes.
## Design and implementation plan
1. New plugin, HTTP/seam, bus metadata and browser files first; run whole new files RED, report
first-run results per mos1272/1590, correct only demonstrably wrong NEW fixtures. No old test edits.
2. Add the three plugin/seam calls and strict HTTP room routes/receipt validation. Reuse existing
principal/write fences. Owner room verbs stay human-only; hide controls from granted users/testers,
and never route a call through an owner transport for them.
3. In broker, extract #withCheck(row), call it for thread messages and each final merged room entry;
extend the existing last-routing condition to room.read. Test real checked reply parity, unchecked
absence and unchanged merged human entry across two members. No broker room.open/send changes.
4. Add /chat/rooms/PROJECT route inside chat.js, sharing its current state, generation/abort handling,
capture/paint/scroll preservation and225 message renderer. Project view gets explicit Open room;
direct room GET only reads and offers Open on room-not-open. Names come from instances catalog,
membership from canonical room threads, with server room.read authoritative. Quiet members stay
available for completion. Human entries display once, with all broker addressees; no client regrouping.
5. Completion parses leading @ tokens with the exact broker grammar and its existing case vectors.
Display typed tags in a preview; use that production tag reader for completion and browser parity.
Suggest members by instance/name, insert the stable instance token, preserve unrelated text/caret.
Arrow keys/Enter/Tab/Escape and click work; labels/text remain escaped and bidi/control inert.
Browser sends original typed body, never a client-selected addressee list. Broker409 lines are fixed.
6. Whole webui/plugins/bus (touched) suites, all12 scripts/test-*.sh under timeout -k10, then isolated
mutants. No live-provider calls (MOSAIC_LIVE_PROVIDER unset). New worker/temp process groups get
killpg cleanup and cwd/Chromium sweep; never touch another seat's processes. Record every red.
7. Hash-pin audit by source path AND old64-hex digest for chat.js/chat-http.mjs/channel module before
in-review. Freeze manifest, push fethry/* only, owner queue move in-review, handoff Boyd+Sage with
candidate/digest; Sage posts because this seat has no Gitea token. No force push, merge or deploy.
Acceptance focus: grouped paging cursor, one entry for a broadcast even across two threads, quiet
member completion and parser parity, checked reply badges/notice safety, malformed receipts/unknown
outcomes,4 refusal codes, no read-side mutation, late reads/sign-out and project draft isolation.
Boundary rules: old-test red, any production path beyond the owned set/amendment, or weakened
assertion goes to Sage BEFORE action. New-file first red is reported then fixture corrected.
## Targeted RED/GREEN receipts (2026-10-24)
Start op fethry-266-start succeeded rev3978 after sage4156's re-pin; old-pin refusal wrote nothing.
First whole NEW files RED: plugin0/1,HTTP0/5,bus1/1,browser0/7, all intended missing-feature paths;
reported fethry0028 before production. Component GREEN plugin1/0,HTTP5/0,bus2/0. Browser5/2 exposed a
missing submit branch in our new room event dispatcher; reported fethry0029, fixed only that branch,
then browser7/0. No fixture/assertion changed to fix production behavior.
Boundary coverage added before full gate: malformed numeric member key was accepted by regex
coercion (new HTTP6/1, fethry0030); required string and unique group ids/addressees, now HTTP7/0.
New browser assertions use fresh send ids and read back exactly one broadcast entry. Expanded run8/1
exposed a nested quote in our new runtime CSS selector; reported fethry0031 BEFORE fixing fixture
with JSON.stringify, same exact count=1 assertion retained. Final browser9/0,4864.326ms, with quiet
member Arrow/Tab/Escape, raw body preservation, no duplicate pending send, uncertain draft, parser
vectors, all4 refusal lines, checked reply/cite/notice safety and late-read/sign-out coverage.
No pre-existing test/helper edit and no assertion loosened. Production set is precisely six brief-
owned paths including the narrow broker amendment. All red/green raw logs under unique TMPDIR.
Full gate now runs run-gate.py: local editor build, whole webui/plugins/bus then all12 test scripts,
serial timeout -k10, per-test30s, MOSAIC_REQUIRE_EDITOR_BUILD=1; live-provider env removed. Each step
has its own process group and killpg cleanup, with signals recorded. Any failure stops for report.
## Full gate first pass
Editor build exit0; whole webui458/0 (270.066s), plugins120/0 (.866s), no skips/cancels.
Bus999/1 (132.9s): OLD library-credential.test.mjs T9 at317 expected library-fetch-no-grant,
received library-sync-busy. Reported fethry0032 before changes. No old test/source change.
Read-only diagnosis: fixture starts a50ms periodic sync; waiting for a completed log does not
prevent the next tick from acquiring running(mirror) before the manual sync. Existing sync.mjs140
checks running before credential refusal. Row266 touches neither timer nor sync implementation.
All12 scripts continue independently; rerun/ruling requested from Sage. Raw bus-full.tap and
gate-summary.json remain in the scratch directory, preserving the red evidence.
Sage4158 confirms the pre-existing50ms T9 timer race and authorizes three unchanged whole-file
credential runs followed by one unchanged whole bus run. Any recurring T9 red stops for Sage;
no fixture or production patch is authorized. The12-script runner proceeds serially first.
All12 scripts/test-*.sh exited0 (scripts-summary.json). Queue's156 tests pass; wrapper28/0
and explicitly skips canonical queue verify/render checks in an isolated worktree. Task92/0
uses existing row244 live-provider exclusions, with MOSAIC_LIVE_PROVIDER unset. No script edited.
Three unchanged whole credential-file reruns under sage4158 each pass6/0 without skips/cancels,
1.367s each wall clock. Full unchanged bus rerun is next/running.
Pin audit before in-review: source paths/basenames and old/current64hex values for chat.js,
chat-http.mjs and channel/webui/index.mjs searched across package/script tests. No literal old
or current digest pin. The10 frozen package hashes include all six production paths and four
new test files; no existing test diff. sha256sum -c passes on all10.
Unchanged full bus rerun GREEN1000/0, no skips/cancels,142.919s. T9 passes. This completes
sage4158's four unchanged green reruns (credential6/0 three times, bus1000/0 once) after the
initial999/1. Preserve all five TAPs. Pre-existing T9 timer race is Sage's follow-up, no source
or old test changed for it. Full applicable suites and12 scripts are green; mutants next.
Evidence display copies strip trailing whitespace only; raw-log-provenance.json records original
paths and both digests. All10 package hashes remain unchanged from full-suite verification.
Source candidate b8b26ae4d. First M01 attempt removed plugin room.open and deterministically
refused channel-operation, but the strict assertion-only classifier did not count the uncaught
PluginError. Restored hash and candidate unchanged. Reported fethry0033 BEFORE strengthening
only the NEW room-channel test with doesNotReject; exact receipt/call assertions retained.
Whole plugins rerun120/0, no skips/cancels. Only that new test hash changes in manifest.r1;
all production hashes, HTTP/bus/browser tests and prior suite coverage remain identical.
Excluded M01 TAP/summary preserved under evidence/mutants-r1, not a killed-mutant claim.
## Final candidate and mutant verification
Tested source3095d6ea5 (b8b26ae4d plus assertion-strengthening new-test follow-up). All16
isolated mutants killed by assertions, exit1 with ERR_ASSERTION, zero skips/cancels, one anchor
each, hash restored after each, candidate hashes unchanged. Evidence/mutants-r2/summary.json
records every result. Initial excluded M01 remains separate. No timeout/error counted as a kill.
All gate/mutant commands bounded with timeout -k10; runner-owned groups cleaned using killpg.
Final /proc sweep finds zero node/Chromium processes with this worktree cwd or command path;
process-sweep.json records it. No other seat process touched.
Final manifest.r1 SHA256:5f816042792b24b790d4faa5bfdc35a8138225819f85d9d32ae49ba8a2de033f,
10 package files. Six production paths, four NEW test paths, zero pre-existing test edits.
No production/source change after suite verification; only the new plugin assertion strengthened
and full plugins rerun120/0. Final static diff and manifest checks pass. Slot can return.
Review first Boyd, then Dewey; Sage owns review-request credential and landing.
Canonical owner move fethry-266-inreview succeeded rev3979: row266 in-review round1 #1771,
frozen manifest5f816042. Review request outcome rev3980 failed solely because
MOSAIC_GITEA_CREDENTIAL_FILE is unset; no credential file read or alternate credential used.
Sage must post the arranged review request. Boyd received direct candidate handoff fethry0035.
Initial duplicate handoff to Sage timed out in T3 metadata GET; retry via outcome report0036.
+34
View File
@@ -0,0 +1,34 @@
# Row266 review packet — Console rooms (#1771)
Owner Fethry14f80e3c; first reviewer Boyd48a3ae38, second Dewey84fb346c.
Base487500a16, pinned amended brief85e6fe2ecfc5. Sage owns gate and landing.
The owner Console has an explicit Open room action and a merged project timeline. Human broadcasts
show once with the broker's addressees; agent rows use the shared225 reply renderer including stored
check metadata, badges, changed-file labels and safe notices. Completion suggests quiet and active room
members and follows the broker's leading-tag grammar. The original body goes to room.send.
The seam has exactly three room calls, and no thread.messages fanout. Four room refusals map to409
with fixed lines. Read-only routes never open rooms. Writes retain existing authority/Origin fences,
strict arguments, receipt validation, draft preservation and no automatic retry after uncertainty.
manifest.r1.sha256 freezes exactly10 package paths: six production files and four NEW test files.
No pre-existing test is edited. Check with sha256sum -c from the repository root. The only brief
amendment is sage4156's narrow broker helper/check/last hydration for room.read; room.open/send and
other broker fields/refusals remain unchanged. Other changes are append-only session/build records
and this tracking packet. No queue JSON, role, policy, secret or generated editor asset changes.
Evidence and RED/GREEN history are in BUILD.md. Full suites/scripts are green; all16 isolated mutants are killed by assertions on source3095d6ea5. The initial
full bus999/1 is the pre-existing T9 timer race, reported before action; unchanged reruns authorized
by sage4158 all pass: credential6/0 three times, bus1000/0 once. Webui458/0, plugins120/0,
all12 scripts exit0. Queue wrapper skips canonical-only verify/render, and task retains the
row244 live-provider exclusions. No test-runner skips/cancellations in package suites. Full plugins120/0 rerun after strengthening
the new dispatch assertion. All16 one-anchor mutants restore and preserve candidate hashes; initial
M01 uncaught-refusal attempt is explicitly excluded. Final own-cwd node/Chromium sweep is empty. Review must consider every run, including the red.
Pin audit: pin-audit-before.txt and pin-audit-after.txt search source paths/basenames and64-hex old
and new digests across tests. No literal digest pin needs updating for any of the three named files.
Review focus: grouped paging cursor, no body-based client regrouping, quiet membership/completion,
raw body preservation, checked/unchecked renderer trust, grant/tester refusal, pending/uncertain writes,
late reads/sign-out and strictly scoped broker hydration.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,16 @@
npm notice run @mosaic/[email protected] build:editor
npm notice run node editor/build.mjs
▲ [WARNING] Ignoring this import because "third_party/editor/prosemirror-transform/src/mark.ts" was marked as having no side effects [ignored-bare-import]
third_party/editor/prosemirror-transform/src/index.ts:10:7:
10 │ import "./mark"
╵ ~~~~~~~~
"sideEffects" is false in the enclosing "package.json" file:
third_party/editor/prosemirror-transform/package.json:13:2:
13 │ "sideEffects": false,
╵ ~~~~~~~~~~~~~
editor.js 9c881b13740fae2408717ad75fa1d85bfcfe3fa8b3af0f1b3f2e706c90e80fee (133 inputs)
editor.css 8654ac57d1e7a561110138ecd7f701c34a074017ff8c4cd2768d8083908ee61f
@@ -0,0 +1,63 @@
[
{
"step": "editor-build",
"command": [
"npm",
"run",
"build:editor"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui",
"outerSeconds": 120,
"exit": 0,
"seconds": 0.214,
"cleanupSignals": []
},
{
"step": "webui-full",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/webui/tests/"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 900,
"exit": 0,
"seconds": 270.066,
"cleanupSignals": []
},
{
"step": "plugins-full",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/plugins/tests/"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 300,
"exit": 0,
"seconds": 0.866,
"cleanupSignals": []
},
{
"step": "bus-full",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/bus/tests/"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 1,
"seconds": 132.9,
"cleanupSignals": []
}
]
@@ -0,0 +1,28 @@
TAP version 13
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
not ok 1 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 0.698734
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-nwewtdks/M01-plugin/packages/plugins/tests/room-channel.test.mjs:5:1'
failureType: 'testCodeFailure'
error: 'unsupported channel operation'
code: 'channel-operation'
name: 'PluginError'
stack: |-
dispatch (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-nwewtdks/M01-plugin/packages/plugins/channel/webui/index.mjs:25:31)
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-nwewtdks/M01-plugin/packages/plugins/tests/room-channel.test.mjs:8:28)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 45.47515
@@ -0,0 +1,35 @@
{
"source": "b8b26ae4d2b440cd14959c4e792baa4b068964e4",
"selected": [
"M01-plugin",
"M02-seam-body",
"M03-page-cursor",
"M04-room-status",
"M05-strict-body",
"M06-receipt-type",
"M07-room-check",
"M08-room-last",
"M09-browser-body",
"M10-members",
"M11-checked-render",
"M12-leading-tags",
"M13-readonly",
"M14-uncertainty",
"M15-pending",
"M16-body-dedup"
],
"candidateUnchanged": true,
"results": [
{
"mutant": "M01-plugin",
"test": "packages/plugins/tests/room-channel.test.mjs",
"exit": 1,
"killed": false,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.115,
"cleanupSignals": []
}
]
}
@@ -0,0 +1,40 @@
TAP version 13
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
not ok 1 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 1.157584
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M01-plugin/packages/plugins/tests/room-channel.test.mjs:5:1'
failureType: 'testCodeFailure'
error: |-
Got unwanted rejection.
Actual message: "unsupported channel operation"
code: 'ERR_ASSERTION'
name: 'AssertionError'
actual:
error: 'unsupported channel operation'
stack: |-
dispatch (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M01-plugin/packages/plugins/channel/webui/index.mjs:25:31)
file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M01-plugin/packages/plugins/tests/room-channel.test.mjs:9:62
waitForActual (node:assert:601:21)
strict.doesNotReject (node:assert:758:39)
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M01-plugin/packages/plugins/tests/room-channel.test.mjs:9:18)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
operator: 'doesNotReject'
stack: |-
async TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M01-plugin/packages/plugins/tests/room-channel.test.mjs:9:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 52.924783
@@ -0,0 +1,126 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
not ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 11.868254
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M02-seam-body/packages/webui/tests/room-http.test.mjs:34:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
'room.send',
{
+ body: '@bean exact\ntext',
- body: '@bean exact\ntext ',
project: '10000000-0000-4000-8000-000000000001'
}
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0: 'room.send'
1:
project: '10000000-0000-4000-8000-000000000001'
body: |-
@bean exact
text
actual:
0: 'room.send'
1:
project: '10000000-0000-4000-8000-000000000001'
body: |-
@bean exact
text
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M02-seam-body/packages/webui/tests/room-http.test.mjs:40:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
not ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 12.337405
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M02-seam-body/packages/webui/tests/room-http.test.mjs:44:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
'room.send',
{
+ body: '@bean\n exact text',
- body: ' \t@bean\n exact text ',
project: '10000000-0000-4000-8000-000000000001'
}
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0: 'room.send'
1:
project: '10000000-0000-4000-8000-000000000001'
body: |-
@bean
exact text
actual:
0: 'room.send'
1:
project: '10000000-0000-4000-8000-000000000001'
body: |-
@bean
exact text
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M02-seam-body/packages/webui/tests/room-http.test.mjs:54:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 10.902017
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 3.766834
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 3.752149
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.085047
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.823543
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 5
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 164.309887
@@ -0,0 +1,72 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 11.193494
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
not ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 13.394594
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M03-page-cursor/packages/webui/tests/room-http.test.mjs:44:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ '30000000-0000-4000-8000-000000000001'
- '30000000-0000-4000-8000-000000000002'
^
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: '30000000-0000-4000-8000-000000000002'
actual: '30000000-0000-4000-8000-000000000001'
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M03-page-cursor/packages/webui/tests/room-http.test.mjs:58:42)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 10.010122
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 4.569671
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 3.093316
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.282426
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.630311
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 154.412627
@@ -0,0 +1,69 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 10.261335
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 11.510403
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.56247
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
not ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 2.373319
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M04-room-status/packages/webui/tests/room-http.test.mjs:75:1'
failureType: 'testCodeFailure'
error: |-
unknown-addressee
400 !== 409
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 409
actual: 400
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M04-room-status/packages/webui/tests/room-http.test.mjs:80:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 4.132728
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.139569
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.748544
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 146.718382
@@ -0,0 +1,69 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 10.171183
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 11.711364
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
not ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 7.485153
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M05-strict-body/packages/webui/tests/room-http.test.mjs:62:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
503 !== 400
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 400
actual: 503
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M05-strict-body/packages/webui/tests/room-http.test.mjs:68:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 3.76857
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 3.713736
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.15552
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.612719
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 143.474549
@@ -0,0 +1,69 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 11.013578
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 12.608774
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.684014
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 3.750164
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 3.555785
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
not ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 1.849296
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M06-receipt-type/packages/webui/tests/room-http.test.mjs:95:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
200 !== 503
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 503
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M06-receipt-type/packages/webui/tests/room-http.test.mjs:99:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.908096
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 150.3236
@@ -0,0 +1,54 @@
TAP version 13
# Subtest: C2-10c room.read carries exactly the thread check and current file last
not ok 1 - C2-10c room.read carries exactly the thread check and current file last
---
duration_ms: 83.507065
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M07-room-check/packages/bus/tests/room-check.test.mjs:10:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
+ undefined
- {
- citations: [
- {
- agent: null,
- file: '451004f1ccad7601e78c1e40eb352997ab433e08',
- last: '451004f1ccad7601e78c1e40eb352997ab433e08',
- scope: 'canon'
- }
- ]
- }
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
citations:
0:
scope: 'canon'
agent: ~
file: '451004f1ccad7601e78c1e40eb352997ab433e08'
last: '451004f1ccad7601e78c1e40eb352997ab433e08'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M07-room-check/packages/bus/tests/room-check.test.mjs:20:10)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
ok 2 - C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
---
duration_ms: 15.38309
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 1
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 171.86807
@@ -0,0 +1,59 @@
TAP version 13
# Subtest: C2-10c room.read carries exactly the thread check and current file last
not ok 1 - C2-10c room.read carries exactly the thread check and current file last
---
duration_ms: 67.328888
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M08-room-last/packages/bus/tests/room-check.test.mjs:10:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
{
citations: [
{
agent: null,
file: '10b4ecc1f7ff568550e9967c36701b5a6bd5e8b6',
- last: '10b4ecc1f7ff568550e9967c36701b5a6bd5e8b6',
scope: 'canon'
}
]
}
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
citations:
0:
scope: 'canon'
agent: ~
file: '10b4ecc1f7ff568550e9967c36701b5a6bd5e8b6'
last: '10b4ecc1f7ff568550e9967c36701b5a6bd5e8b6'
actual:
citations:
0:
scope: 'canon'
agent: ~
file: '10b4ecc1f7ff568550e9967c36701b5a6bd5e8b6'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M08-room-last/packages/bus/tests/room-check.test.mjs:20:10)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
ok 2 - C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
---
duration_ms: 15.525552
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 1
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 166.159314
@@ -0,0 +1,101 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 654.717114
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 556.312248
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
not ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 578.59092
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M09-browser-body/packages/webui/tests/room-browser.test.mjs:103:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
{
body: {
+ body: '@bean\n@chet unchanged text'
- body: ' \t@bean\n@chet unchanged text '
},
path: '/api/chat/rooms/10000000-0000-4000-8000-000000000001/messages',
redirect: 'manual'
}
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
path: '/api/chat/rooms/10000000-0000-4000-8000-000000000001/messages'
body:
body: |-
@bean
@chet unchanged text
redirect: 'manual'
actual:
path: '/api/chat/rooms/10000000-0000-4000-8000-000000000001/messages'
body:
body: |-
@bean
@chet unchanged text
redirect: 'manual'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M09-browser-body/packages/webui/tests/room-browser.test.mjs:116:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 650.144341
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 548.873708
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 555.91075
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 618.749654
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 592.109857
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 543.701394
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5408.909271
@@ -0,0 +1,91 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 586.051382
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 627.02031
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 608.059981
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 610.484584
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 597.77347
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 484.861175
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 499.306807
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
not ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 593.020195
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M10-members/packages/webui/tests/room-browser.test.mjs:172:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
'bean',
'chet',
+ 'outsider'
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0: 'bean'
1: 'chet'
actual:
0: 'bean'
1: 'chet'
2: 'outsider'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M10-members/packages/webui/tests/room-browser.test.mjs:175:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 595.947751
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5316.831255
@@ -0,0 +1,83 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
not ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 492.369887
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M11-checked-render/packages/webui/tests/room-browser.test.mjs:79:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ null
- 'supporting'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 'supporting'
actual: ~
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M11-checked-render/packages/webui/tests/room-browser.test.mjs:85:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 560.879236
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 632.100162
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 557.502883
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 577.812754
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 571.055252
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 562.265422
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 562.101368
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 526.843257
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5152.235957
@@ -0,0 +1,88 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 533.524565
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 565.530318
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 592.975458
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
not ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 638.012095
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M12-leading-tags/packages/webui/tests/room-browser.test.mjs:125:1'
failureType: 'testCodeFailure'
error: |-
leading whitespace
+ actual - expected
+ []
- [
- 'bean',
- 'chet'
- ]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0: 'bean'
1: 'chet'
actual:
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M12-leading-tags/packages/webui/tests/room-browser.test.mjs:129:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 613.741071
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 582.549802
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 620.515287
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 476.797273
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 568.856603
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5305.643765
@@ -0,0 +1,81 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 515.306411
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 587.728142
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 615.408914
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 631.443015
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 548.976444
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
not ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 482.66679
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M13-readonly/packages/webui/tests/room-browser.test.mjs:149:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
1 !== 0
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 0
actual: 1
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M13-readonly/packages/webui/tests/room-browser.test.mjs:155:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 581.912482
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 587.019049
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 603.816605
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5264.797509
@@ -0,0 +1,86 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 10.260746
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 11.451922
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.444913
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
not ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 2.363579
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M14-uncertainty/packages/webui/tests/room-http.test.mjs:75:1'
failureType: 'testCodeFailure'
error: |-
unknown-addressee
400 !== 409
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 409
actual: 400
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M14-uncertainty/packages/webui/tests/room-http.test.mjs:80:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
not ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 4.236784
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M14-uncertainty/packages/webui/tests/room-http.test.mjs:85:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
400 !== 503
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 503
actual: 400
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M14-uncertainty/packages/webui/tests/room-http.test.mjs:90:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.244565
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.63121
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 5
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 145.498511
@@ -0,0 +1,81 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 642.500258
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 681.379212
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 591.532251
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 554.2939
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 615.041822
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 477.706395
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 589.566493
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 473.941034
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
not ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 512.018276
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M15-pending/packages/webui/tests/room-browser.test.mjs:183:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
2 !== 1
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 1
actual: 2
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M15-pending/packages/webui/tests/room-browser.test.mjs:189:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5248.865109
@@ -0,0 +1,98 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
not ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 474.489714
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M16-body-dedup/packages/webui/tests/room-browser.test.mjs:79:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
4 !== 5
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 5
actual: 4
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M16-body-dedup/packages/webui/tests/room-browser.test.mjs:82:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 587.106171
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
not ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 506.06589
type: 'test'
location: '/mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M16-body-dedup/packages/webui/tests/room-browser.test.mjs:103:1'
failureType: 'testCodeFailure'
error: |-
no optimistic copies of a broadcast
4 !== 5
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 5
actual: 4
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/KV9rHb/fethry266-mutants-32bmgm_x/M16-body-dedup/packages/webui/tests/room-browser.test.mjs:118:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 574.421619
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 525.669748
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 574.584051
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 714.522652
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 605.062154
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 598.986047
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 7
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5270.931167
@@ -0,0 +1,200 @@
{
"source": "3095d6ea5295d19e216128454c33b616b16d8d34",
"selected": [
"M01-plugin",
"M02-seam-body",
"M03-page-cursor",
"M04-room-status",
"M05-strict-body",
"M06-receipt-type",
"M07-room-check",
"M08-room-last",
"M09-browser-body",
"M10-members",
"M11-checked-render",
"M12-leading-tags",
"M13-readonly",
"M14-uncertainty",
"M15-pending",
"M16-body-dedup"
],
"candidateUnchanged": true,
"results": [
{
"mutant": "M01-plugin",
"test": "packages/plugins/tests/room-channel.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.115,
"cleanupSignals": []
},
{
"mutant": "M02-seam-body",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M03-page-cursor",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M04-room-status",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M05-strict-body",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M06-receipt-type",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M07-room-check",
"test": "packages/bus/tests/room-check.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.216,
"cleanupSignals": []
},
{
"mutant": "M08-room-last",
"test": "packages/bus/tests/room-check.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.216,
"cleanupSignals": []
},
{
"mutant": "M09-browser-body",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.478,
"cleanupSignals": []
},
{
"mutant": "M10-members",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.377,
"cleanupSignals": []
},
{
"mutant": "M11-checked-render",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.227,
"cleanupSignals": []
},
{
"mutant": "M12-leading-tags",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.376,
"cleanupSignals": []
},
{
"mutant": "M13-readonly",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.326,
"cleanupSignals": []
},
{
"mutant": "M14-uncertainty",
"test": "packages/webui/tests/room-http.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 0.215,
"cleanupSignals": []
},
{
"mutant": "M15-pending",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.327,
"cleanupSignals": []
},
{
"mutant": "M16-body-dedup",
"test": "packages/webui/tests/room-browser.test.mjs",
"exit": 1,
"killed": true,
"hits": 1,
"restored": true,
"candidateUnchanged": true,
"seconds": 5.327,
"cleanupSignals": []
}
]
}
@@ -0,0 +1,16 @@
TAP version 13
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
ok 1 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 1.543022
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 45.19296
@@ -0,0 +1,28 @@
TAP version 13
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
not ok 1 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 0.69237
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/plugins/tests/room-channel.test.mjs:5:1'
failureType: 'testCodeFailure'
error: 'unsupported channel operation'
code: 'channel-operation'
name: 'PluginError'
stack: |-
dispatch (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/plugins/channel/webui/index.mjs:23:31)
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/plugins/tests/room-channel.test.mjs:8:28)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 46.133256
@@ -0,0 +1,730 @@
TAP version 13
# Subtest: channel registry requires dispatch at interface 1 and switches a list
ok 1 - channel registry requires dispatch at interface 1 and switches a list
---
duration_ms: 1.255032
type: 'test'
...
# Subtest: channel shipped modules load and preserve every supported operation and bus refusal
ok 2 - channel shipped modules load and preserve every supported operation and bus refusal
---
duration_ms: 2.804618
type: 'test'
...
# Subtest: channel snapshot is a frozen matched pair and a captured array survives reload
ok 3 - channel snapshot is a frozen matched pair and a captured array survives reload
---
duration_ms: 4.163001
type: 'test'
...
# Subtest: channel guards refuse bad names, missing paths, exports and versions while retaining last good
ok 4 - channel guards refuse bad names, missing paths, exports and versions while retaining last good
---
duration_ms: 3.559755
type: 'test'
...
# Subtest: channel guards refuse symlinked module, index and kind directories
ok 5 - channel guards refuse symlinked module, index and kind directories
---
duration_ms: 2.649428
type: 'test'
...
# Subtest: channel caller re-reads settings and keeps off switches independent by module name
ok 6 - channel caller re-reads settings and keeps off switches independent by module name
---
duration_ms: 1.86561
type: 'test'
...
# Subtest: channel caller malformed settings refuse without a chat operation and no unrelated kind loads
ok 7 - channel caller malformed settings refuse without a chat operation and no unrelated kind loads
---
duration_ms: 0.553009
type: 'test'
...
# Subtest: channel caller uses last good after loader refusal and fails closed on first load
ok 8 - channel caller uses last good after loader refusal and fails closed on first load
---
duration_ms: 0.753171
type: 'test'
...
# Subtest: channel dispatch holds no selection lock while a slow send is awaiting
ok 9 - channel dispatch holds no selection lock while a slow send is awaiting
---
duration_ms: 0.63183
type: 'test'
...
# Subtest: channel last realpath check refuses an index swapped after component checks
ok 10 - channel last realpath check refuses an index swapped after component checks
---
duration_ms: 3.292656
type: 'test'
...
# Subtest: channel caller refuses invalid core construction
ok 11 - channel caller refuses invalid core construction
---
duration_ms: 0.192852
type: 'test'
...
# Subtest: channel caller invokes the selected loaded module instead of bypassing dispatch
ok 12 - channel caller invokes the selected loaded module instead of bypassing dispatch
---
duration_ms: 2.4109
type: 'test'
...
# Subtest: channel caller keeps the selected module operation filter in front of the core transport
ok 13 - channel caller keeps the selected module operation filter in front of the core transport
---
duration_ms: 0.296323
type: 'test'
...
# Subtest: REQ-LIMIT-2 daily-quota exports interface version 2 with mayLaunch, mustStop and report
ok 14 - REQ-LIMIT-2 daily-quota exports interface version 2 with mayLaunch, mustStop and report
---
duration_ms: 1.604743
type: 'test'
...
# Subtest: REQ-LIMIT-2 the count sums every kind of the account's known records since the later of the day start and since
ok 15 - REQ-LIMIT-2 the count sums every kind of the account's known records since the later of the day start and since
---
duration_ms: 18.301118
type: 'test'
...
# Subtest: REQ-LIMIT-2 a launch with no account (pi) is covered by no daily quota
ok 16 - REQ-LIMIT-2 a launch with no account (pi) is covered by no daily quota
---
duration_ms: 0.67675
type: 'test'
...
# Subtest: REQ-LIMIT-4 an unknown turn adds nothing and is counted apart
ok 17 - REQ-LIMIT-4 an unknown turn adds nothing and is counted apart
---
duration_ms: 0.68869
type: 'test'
...
# Subtest: REQ-LIMIT-2 the day turns at the reset in its zone, not in UTC, and before it the day is yesterday's
ok 18 - REQ-LIMIT-2 the day turns at the reset in its zone, not in UTC, and before it the day is yesterday's
---
duration_ms: 0.910567
type: 'test'
...
# Subtest: REQ-LIMIT-2 the spring DST change makes the day after a 00:00 reset 23 hours long
ok 19 - REQ-LIMIT-2 the spring DST change makes the day after a 00:00 reset 23 hours long
---
duration_ms: 0.655694
type: 'test'
...
# Subtest: REQ-LIMIT-2 the fall DST change makes the day after a 00:00 reset 25 hours long
ok 20 - REQ-LIMIT-2 the fall DST change makes the day after a 00:00 reset 25 hours long
---
duration_ms: 0.494632
type: 'test'
...
# Subtest: REQ-LIMIT-2 a reset the clocks skip starts the day at the first instant after the gap
ok 21 - REQ-LIMIT-2 a reset the clocks skip starts the day at the first instant after the gap
---
duration_ms: 2.195628
type: 'test'
...
# Subtest: REQ-LIMIT-2 a reset the clocks repeat starts one day, at the first one
ok 22 - REQ-LIMIT-2 a reset the clocks repeat starts one day, at the first one
---
duration_ms: 0.909074
type: 'test'
...
# Subtest: REQ-LIMIT-2 a launch is refused at the quota, a turn that reaches it stops, and the next day allows again
ok 23 - REQ-LIMIT-2 a launch is refused at the quota, a turn that reaches it stops, and the next day allows again
---
duration_ms: 1.793159
type: 'test'
...
# Subtest: REQ-LIMIT-2 counts past the safe integer range come back as digit strings
ok 24 - REQ-LIMIT-2 counts past the safe integer range come back as digit strings
---
duration_ms: 0.560099
type: 'test'
...
# Subtest: REQ-LIMIT-4 bad leaves, records or times throw a code, and the check reads nothing but its arguments
ok 25 - REQ-LIMIT-4 bad leaves, records or times throw a code, and the check reads nothing but its arguments
---
duration_ms: 3.066376
type: 'test'
...
# Subtest: REQ-LIMIT-2 no daily quotas: every launch is allowed and the report is empty
ok 26 - REQ-LIMIT-2 no daily quotas: every launch is allowed and the report is empty
---
duration_ms: 0.26005
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: plain
ok 27 - row 307 pi answers what row 270 read: plain
---
duration_ms: 3.128746
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: budget
ok 28 - row 307 pi answers what row 270 read: budget
---
duration_ms: 1.055314
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: cap
ok 29 - row 307 pi answers what row 270 read: cap
---
duration_ms: 0.646932
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: shrank
ok 30 - row 307 pi answers what row 270 read: shrank
---
duration_ms: 0.514604
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: header
ok 31 - row 307 pi answers what row 270 read: header
---
duration_ms: 0.71648
type: 'test'
...
# Subtest: row 307 pi's first collection over Claude history answers harness-unsupported without a scan, and a later one scans
ok 32 - row 307 pi's first collection over Claude history answers harness-unsupported without a scan, and a later one scans
---
duration_ms: 1.146063
type: 'test'
...
# Subtest: row 307 claude-code reads nothing and answers harness-unsupported with no version
ok 33 - row 307 claude-code reads nothing and answers harness-unsupported with no version
---
duration_ms: 0.242405
type: 'test'
...
# Subtest: row 307 a harness module without compactions doesn't load: missing-export
ok 34 - row 307 a harness module without compactions doesn't load: missing-export
---
duration_ms: 3.973295
type: 'test'
...
# Subtest: row 65 both harness modules load through the loader with every export the kind names
ok 35 - row 65 both harness modules load through the loader with every export the kind names
---
duration_ms: 10.826134
type: 'test'
...
# Subtest: row 65 each module answers for its own harness: adapter, levels, account, web, turn end and session files
ok 36 - row 65 each module answers for its own harness: adapter, levels, account, web, turn end and session files
---
duration_ms: 0.901435
type: 'test'
...
# Subtest: row 65 claude-code preflight: a missing credential, then a missing or relative binary, each refuse with their code
ok 37 - row 65 claude-code preflight: a missing credential, then a missing or relative binary, each refuse with their code
---
duration_ms: 4.184201
type: 'test'
...
# Subtest: row 65 claude-code version asks MOSAIC_CLAUDE_BIN when set, else claude on PATH, else null
ok 38 - row 65 claude-code version asks MOSAIC_CLAUDE_BIN when set, else claude on PATH, else null
---
duration_ms: 4.810847
type: 'test'
...
# Subtest: row 65 pi preflight passes with the repository's pi, and its version is the package pin
ok 39 - row 65 pi preflight passes with the repository's pi, and its version is the package pin
---
duration_ms: 1.138555
type: 'test'
...
# Subtest: row 65 env passes the thinking level to both adapters, and claude-code's bundle writes its hook and MCP files
ok 40 - row 65 env passes the thinking level to both adapters, and claude-code's bundle writes its hook and MCP files
---
duration_ms: 1.575693
type: 'test'
...
# Subtest: row 65 a route's thinking reaches pi as --thinking and claude as --effort, through each module's env and adapter
ok 41 - row 65 a route's thinking reaches pi as --thinking and claude as --effort, through each module's env and adapter
---
duration_ms: 19.982183
type: 'test'
...
# Subtest: row 65 no core source outside packages/plugins names a harness, apart from the listed exceptions
ok 42 - row 65 no core source outside packages/plugins names a harness, apart from the listed exceptions
---
duration_ms: 35.700253
type: 'test'
...
# Subtest: row 65 the grep's pattern catches a quoted name and a regex alternation, and not a word in prose
ok 43 - row 65 the grep's pattern catches a quoted name and a regex alternation, and not a word in prose
---
duration_ms: 0.194718
type: 'test'
...
# Subtest: REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source at interface version 1, limit-check at 2 (row 125), each a list
ok 44 - REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source at interface version 1, limit-check at 2 (row 125), each a list
---
duration_ms: 1.459893
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins accepts one key per kind, each a list of names, empty included
ok 45 - REQ-PLUG-1 checkPlugins accepts one key per kind, each a list of names, empty included
---
duration_ms: 0.318081
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates
ok 46 - REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates
---
duration_ms: 4.006863
type: 'test'
...
# Subtest: REQ-PLUG-1 a kind that switches on one module takes one name, not a list
ok 47 - REQ-PLUG-1 a kind that switches on one module takes one name, not a list
---
duration_ms: 0.240242
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins reports through the caller's fail callback
ok 48 - REQ-PLUG-1 checkPlugins reports through the caller's fail callback
---
duration_ms: 0.227037
type: 'test'
...
# Subtest: REQ-PLUG-1 fixture modules of each kind switch on from a test organization file, no core file changed
ok 49 - REQ-PLUG-1 fixture modules of each kind switch on from a test organization file, no core file changed
---
duration_ms: 10.556155
type: 'test'
...
# Subtest: REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting
ok 50 - REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting
---
duration_ms: 4.644716
type: 'test'
...
# Subtest: REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting
ok 51 - REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting
---
duration_ms: 4.413168
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting
ok 52 - REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting
---
duration_ms: 2.976821
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting
ok 53 - REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting
---
duration_ms: 2.66049
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it)
ok 54 - REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it)
---
duration_ms: 31.929167
type: 'test'
...
# Subtest: REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check
ok 55 - REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check
---
duration_ms: 3.775138
type: 'test'
...
# Subtest: REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting
ok 56 - REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting
---
duration_ms: 3.568356
type: 'test'
...
# Subtest: REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath
ok 57 - REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath
---
duration_ms: 3.240777
type: 'test'
...
# Subtest: REQ-PLUG-1 a module that fails to load refuses and keeps the old setting
ok 58 - REQ-PLUG-1 a module that fails to load refuses and keeps the old setting
---
duration_ms: 3.317002
type: 'test'
...
# Subtest: REQ-PLUG-1 a missing export refuses and keeps the old setting
ok 59 - REQ-PLUG-1 a missing export refuses and keeps the old setting
---
duration_ms: 5.632466
type: 'test'
...
# Subtest: REQ-PLUG-1 a different interface version refuses and keeps the old setting
ok 60 - REQ-PLUG-1 a different interface version refuses and keeps the old setting
---
duration_ms: 5.597735
type: 'test'
...
# Subtest: REQ-PLUG-1 REQ-LIMIT-3 a version 1 limit check refuses after the bump to 2, and the old setting stays
ok 61 - REQ-PLUG-1 REQ-LIMIT-3 a version 1 limit check refuses after the bump to 2, and the old setting stays
---
duration_ms: 4.383909
type: 'test'
...
# Subtest: REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name
ok 62 - REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name
---
duration_ms: 3.685614
type: 'test'
...
# Subtest: REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything
ok 63 - REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything
---
duration_ms: 5.345608
type: 'test'
...
# Subtest: REQ-PLUG-1 loads run one at a time, so the last requested setting wins
ok 64 - REQ-PLUG-1 loads run one at a time, so the last requested setting wins
---
duration_ms: 105.419451
type: 'test'
...
# Subtest: REQ-PLUG-1 the loader needs a log callback
ok 65 - REQ-PLUG-1 the loader needs a log callback
---
duration_ms: 1.585267
type: 'test'
...
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
ok 66 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 5.236914
type: 'test'
...
# Subtest: REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers
ok 67 - REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers
---
duration_ms: 3.225057
type: 'test'
...
# Subtest: REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws
ok 68 - REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws
---
duration_ms: 1.278205
type: 'test'
...
# Subtest: REQ-PLUG-2 search-source takes one name, not a list
ok 69 - REQ-PLUG-2 search-source takes one name, not a list
---
duration_ms: 0.269767
type: 'test'
...
# Subtest: REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng
ok 70 - REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng
---
duration_ms: 1.229975
type: 'test'
...
# Subtest: REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called
ok 71 - REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called
---
duration_ms: 0.882877
type: 'test'
...
# Subtest: the searxng modules end the request when the broker's AbortSignal fires
ok 72 - the searxng modules end the request when the broker's AbortSignal fires
---
duration_ms: 41.188565
type: 'test'
...
# Subtest: hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name
ok 73 - hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name
---
duration_ms: 0.618514
type: 'test'
...
# Subtest: REQ-LIMIT-3 token-limit exports interface version 2 with mayLaunch, mustStop and report
ok 74 - REQ-LIMIT-3 token-limit exports interface version 2 with mayLaunch, mustStop and report
---
duration_ms: 4.517556
type: 'test'
...
# Subtest: REQ-LIMIT-3 each subject kind refuses at its limit and allows under it
ok 75 - REQ-LIMIT-3 each subject kind refuses at its limit and allows under it
---
duration_ms: 1.445175
type: 'test'
...
# Subtest: REQ-LIMIT-3 a limit covers only its own subject's records and launches
ok 76 - REQ-LIMIT-3 a limit covers only its own subject's records and launches
---
duration_ms: 0.361027
type: 'test'
...
# Subtest: REQ-LIMIT-4 only known records count; an unknown turn adds nothing and is counted apart
ok 77 - REQ-LIMIT-4 only known records count; an unknown turn adds nothing and is counted apart
---
duration_ms: 0.288366
type: 'test'
...
# Subtest: REQ-LIMIT-3 a reset moves since: records before it no longer count
ok 78 - REQ-LIMIT-3 a reset moves since: records before it no longer count
---
duration_ms: 0.203004
type: 'test'
...
# Subtest: REQ-LIMIT-3 limit 0 refuses with no records; the first limit hit in file order is named
ok 79 - REQ-LIMIT-3 limit 0 refuses with no records; the first limit hit in file order is named
---
duration_ms: 0.160925
type: 'test'
...
# Subtest: REQ-LIMIT-3 a key limit is not enforced while every record's account is null (decision 150 Q1)
ok 80 - REQ-LIMIT-3 a key limit is not enforced while every record's account is null (decision 150 Q1)
---
duration_ms: 0.192387
type: 'test'
...
# Subtest: REQ-LIMIT-3 counts past the safe integer range come back as digit strings
ok 81 - REQ-LIMIT-3 counts past the safe integer range come back as digit strings
---
duration_ms: 0.190253
type: 'test'
...
# Subtest: REQ-LIMIT-4 bad records or times throw a code, and the check reads nothing but its arguments
ok 82 - REQ-LIMIT-4 bad records or times throw a code, and the check reads nothing but its arguments
---
duration_ms: 0.66893
type: 'test'
...
# Subtest: REQ-LIMIT-3 no token limits: every launch is allowed and the report is empty
ok 83 - REQ-LIMIT-3 no token limits: every launch is allowed and the report is empty
---
duration_ms: 0.264938
type: 'test'
...
# Subtest: REQ-LIMIT-3 token-limit ignores tokens.daily, which the daily-quota check enforces (row 126)
ok 84 - REQ-LIMIT-3 token-limit ignores tokens.daily, which the daily-quota check enforces (row 126)
---
duration_ms: 0.52878
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi: reports all token kinds and deduplicates assistant ids
ok 85 - REQ-LIMIT-4 pi: reports all token kinds and deduplicates assistant ids
---
duration_ms: 3.202577
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code: reports all token kinds and deduplicates assistant ids
ok 86 - REQ-LIMIT-4 claude-code: reports all token kinds and deduplicates assistant ids
---
duration_ms: 0.789693
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi cut line is unknown, never zero or content
ok 87 - REQ-LIMIT-4 pi cut line is unknown, never zero or content
---
duration_ms: 1.068437
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi invalid UTF-8 is unknown, never zero or content
ok 88 - REQ-LIMIT-4 pi invalid UTF-8 is unknown, never zero or content
---
duration_ms: 0.551999
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi missing input is unknown, never zero or content
ok 89 - REQ-LIMIT-4 pi missing input is unknown, never zero or content
---
duration_ms: 0.585082
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi negative count is unknown, never zero or content
ok 90 - REQ-LIMIT-4 pi negative count is unknown, never zero or content
---
duration_ms: 0.498103
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi fractional count is unknown, never zero or content
ok 91 - REQ-LIMIT-4 pi fractional count is unknown, never zero or content
---
duration_ms: 0.451081
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi unsafe integer is unknown, never zero or content
ok 92 - REQ-LIMIT-4 pi unsafe integer is unknown, never zero or content
---
duration_ms: 0.467648
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi file size is unknown, never zero or content
ok 93 - REQ-LIMIT-4 pi file size is unknown, never zero or content
---
duration_ms: 12.153053
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi line size is unknown, never zero or content
ok 94 - REQ-LIMIT-4 pi line size is unknown, never zero or content
---
duration_ms: 3.738483
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi symlink file is unknown, never zero or content
ok 95 - REQ-LIMIT-4 pi symlink file is unknown, never zero or content
---
duration_ms: 0.51929
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi hardlink file is unknown, never zero or content
ok 96 - REQ-LIMIT-4 pi hardlink file is unknown, never zero or content
---
duration_ms: 0.332686
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code cut line is unknown, never zero or content
ok 97 - REQ-LIMIT-4 claude-code cut line is unknown, never zero or content
---
duration_ms: 0.375612
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code invalid UTF-8 is unknown, never zero or content
ok 98 - REQ-LIMIT-4 claude-code invalid UTF-8 is unknown, never zero or content
---
duration_ms: 0.399894
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code missing input is unknown, never zero or content
ok 99 - REQ-LIMIT-4 claude-code missing input is unknown, never zero or content
---
duration_ms: 0.377755
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code negative count is unknown, never zero or content
ok 100 - REQ-LIMIT-4 claude-code negative count is unknown, never zero or content
---
duration_ms: 0.370714
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code fractional count is unknown, never zero or content
ok 101 - REQ-LIMIT-4 claude-code fractional count is unknown, never zero or content
---
duration_ms: 0.359511
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code unsafe integer is unknown, never zero or content
ok 102 - REQ-LIMIT-4 claude-code unsafe integer is unknown, never zero or content
---
duration_ms: 0.372877
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code file size is unknown, never zero or content
ok 103 - REQ-LIMIT-4 claude-code file size is unknown, never zero or content
---
duration_ms: 5.929833
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code line size is unknown, never zero or content
ok 104 - REQ-LIMIT-4 claude-code line size is unknown, never zero or content
---
duration_ms: 2.390572
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code symlink file is unknown, never zero or content
ok 105 - REQ-LIMIT-4 claude-code symlink file is unknown, never zero or content
---
duration_ms: 0.422073
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code hardlink file is unknown, never zero or content
ok 106 - REQ-LIMIT-4 claude-code hardlink file is unknown, never zero or content
---
duration_ms: 0.361017
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi a symlink directory anywhere in the root refuses
ok 107 - REQ-LIMIT-4 pi a symlink directory anywhere in the root refuses
---
duration_ms: 0.368549
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code a symlink directory anywhere in the root refuses
ok 108 - REQ-LIMIT-4 claude-code a symlink directory anywhere in the root refuses
---
duration_ms: 0.289802
type: 'test'
...
# Subtest: REQ-LIMIT-4 bounded directory entries refuse before scanning an arbitrary tree
ok 109 - REQ-LIMIT-4 bounded directory entries refuse before scanning an arbitrary tree
---
duration_ms: 7.765819
type: 'test'
...
# Subtest: REQ-LIMIT-4 conflicting repeated usage refuses rather than double counting
ok 110 - REQ-LIMIT-4 conflicting repeated usage refuses rather than double counting
---
duration_ms: 0.531022
type: 'test'
...
# Subtest: REQ-LIMIT-4 oversized metadata refuses before reading any transcript bytes
ok 111 - REQ-LIMIT-4 oversized metadata refuses before reading any transcript bytes
---
duration_ms: 6.610835
type: 'test'
...
# Subtest: REQ-LIMIT-4 a file growing after fstat cannot exceed the streaming byte budget
ok 112 - REQ-LIMIT-4 a file growing after fstat cannot exceed the streaming byte budget
---
duration_ms: 6.282821
type: 'test'
...
# Subtest: REQ-LIMIT-4 a same-size edit during a read is unknown rather than a stable snapshot
ok 113 - REQ-LIMIT-4 a same-size edit during a read is unknown rather than a stable snapshot
---
duration_ms: 0.810788
type: 'test'
...
# Subtest: REQ-LIMIT-4 record count is bounded even when each record is tiny
ok 114 - REQ-LIMIT-4 record count is bounded even when each record is tiny
---
duration_ms: 7.805423
type: 'test'
...
# Subtest: REQ-LIMIT-4 missing assistant usage, malformed JSON and conflicting identities are unknown
ok 115 - REQ-LIMIT-4 missing assistant usage, malformed JSON and conflicting identities are unknown
---
duration_ms: 0.928936
type: 'test'
...
# Subtest: REQ-LIMIT-4 caller roots, unsupported harnesses and non-object JSON never become zero usage
ok 116 - REQ-LIMIT-4 caller roots, unsupported harnesses and non-object JSON never become zero usage
---
duration_ms: 0.621657
type: 'test'
...
# Subtest: REQ-LIMIT-4 even a complete JSON record with no final newline is an unknown cut line
ok 117 - REQ-LIMIT-4 even a complete JSON record with no final newline is an unknown cut line
---
duration_ms: 0.310336
type: 'test'
...
# Subtest: REQ-LIMIT-4 final file opens are nonblocking before hostile file type is known
ok 118 - REQ-LIMIT-4 final file opens are nonblocking before hostile file type is known
---
duration_ms: 0.533184
type: 'test'
...
# Subtest: REQ-LIMIT-4 invalid transcript entry names refuse before file lookup
ok 119 - REQ-LIMIT-4 invalid transcript entry names refuse before file lookup
---
duration_ms: 0.316982
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code refuses a symlinked project directory
ok 120 - REQ-LIMIT-4 claude-code refuses a symlinked project directory
---
duration_ms: 0.474377
type: 'test'
...
1..120
# tests 120
# suites 0
# pass 120
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1004.179548
@@ -0,0 +1,730 @@
TAP version 13
# Subtest: channel registry requires dispatch at interface 1 and switches a list
ok 1 - channel registry requires dispatch at interface 1 and switches a list
---
duration_ms: 1.093033
type: 'test'
...
# Subtest: channel shipped modules load and preserve every supported operation and bus refusal
ok 2 - channel shipped modules load and preserve every supported operation and bus refusal
---
duration_ms: 2.329081
type: 'test'
...
# Subtest: channel snapshot is a frozen matched pair and a captured array survives reload
ok 3 - channel snapshot is a frozen matched pair and a captured array survives reload
---
duration_ms: 3.672804
type: 'test'
...
# Subtest: channel guards refuse bad names, missing paths, exports and versions while retaining last good
ok 4 - channel guards refuse bad names, missing paths, exports and versions while retaining last good
---
duration_ms: 3.25585
type: 'test'
...
# Subtest: channel guards refuse symlinked module, index and kind directories
ok 5 - channel guards refuse symlinked module, index and kind directories
---
duration_ms: 2.246923
type: 'test'
...
# Subtest: channel caller re-reads settings and keeps off switches independent by module name
ok 6 - channel caller re-reads settings and keeps off switches independent by module name
---
duration_ms: 1.351401
type: 'test'
...
# Subtest: channel caller malformed settings refuse without a chat operation and no unrelated kind loads
ok 7 - channel caller malformed settings refuse without a chat operation and no unrelated kind loads
---
duration_ms: 0.435438
type: 'test'
...
# Subtest: channel caller uses last good after loader refusal and fails closed on first load
ok 8 - channel caller uses last good after loader refusal and fails closed on first load
---
duration_ms: 0.580004
type: 'test'
...
# Subtest: channel dispatch holds no selection lock while a slow send is awaiting
ok 9 - channel dispatch holds no selection lock while a slow send is awaiting
---
duration_ms: 0.491762
type: 'test'
...
# Subtest: channel last realpath check refuses an index swapped after component checks
ok 10 - channel last realpath check refuses an index swapped after component checks
---
duration_ms: 2.241815
type: 'test'
...
# Subtest: channel caller refuses invalid core construction
ok 11 - channel caller refuses invalid core construction
---
duration_ms: 0.150251
type: 'test'
...
# Subtest: channel caller invokes the selected loaded module instead of bypassing dispatch
ok 12 - channel caller invokes the selected loaded module instead of bypassing dispatch
---
duration_ms: 2.065095
type: 'test'
...
# Subtest: channel caller keeps the selected module operation filter in front of the core transport
ok 13 - channel caller keeps the selected module operation filter in front of the core transport
---
duration_ms: 0.241841
type: 'test'
...
# Subtest: REQ-LIMIT-2 daily-quota exports interface version 2 with mayLaunch, mustStop and report
ok 14 - REQ-LIMIT-2 daily-quota exports interface version 2 with mayLaunch, mustStop and report
---
duration_ms: 4.156727
type: 'test'
...
# Subtest: REQ-LIMIT-2 the count sums every kind of the account's known records since the later of the day start and since
ok 15 - REQ-LIMIT-2 the count sums every kind of the account's known records since the later of the day start and since
---
duration_ms: 12.582509
type: 'test'
...
# Subtest: REQ-LIMIT-2 a launch with no account (pi) is covered by no daily quota
ok 16 - REQ-LIMIT-2 a launch with no account (pi) is covered by no daily quota
---
duration_ms: 0.329543
type: 'test'
...
# Subtest: REQ-LIMIT-4 an unknown turn adds nothing and is counted apart
ok 17 - REQ-LIMIT-4 an unknown turn adds nothing and is counted apart
---
duration_ms: 0.442314
type: 'test'
...
# Subtest: REQ-LIMIT-2 the day turns at the reset in its zone, not in UTC, and before it the day is yesterday's
ok 18 - REQ-LIMIT-2 the day turns at the reset in its zone, not in UTC, and before it the day is yesterday's
---
duration_ms: 0.682365
type: 'test'
...
# Subtest: REQ-LIMIT-2 the spring DST change makes the day after a 00:00 reset 23 hours long
ok 19 - REQ-LIMIT-2 the spring DST change makes the day after a 00:00 reset 23 hours long
---
duration_ms: 0.387921
type: 'test'
...
# Subtest: REQ-LIMIT-2 the fall DST change makes the day after a 00:00 reset 25 hours long
ok 20 - REQ-LIMIT-2 the fall DST change makes the day after a 00:00 reset 25 hours long
---
duration_ms: 0.330814
type: 'test'
...
# Subtest: REQ-LIMIT-2 a reset the clocks skip starts the day at the first instant after the gap
ok 21 - REQ-LIMIT-2 a reset the clocks skip starts the day at the first instant after the gap
---
duration_ms: 1.372326
type: 'test'
...
# Subtest: REQ-LIMIT-2 a reset the clocks repeat starts one day, at the first one
ok 22 - REQ-LIMIT-2 a reset the clocks repeat starts one day, at the first one
---
duration_ms: 0.4571
type: 'test'
...
# Subtest: REQ-LIMIT-2 a launch is refused at the quota, a turn that reaches it stops, and the next day allows again
ok 23 - REQ-LIMIT-2 a launch is refused at the quota, a turn that reaches it stops, and the next day allows again
---
duration_ms: 1.027459
type: 'test'
...
# Subtest: REQ-LIMIT-2 counts past the safe integer range come back as digit strings
ok 24 - REQ-LIMIT-2 counts past the safe integer range come back as digit strings
---
duration_ms: 0.350076
type: 'test'
...
# Subtest: REQ-LIMIT-4 bad leaves, records or times throw a code, and the check reads nothing but its arguments
ok 25 - REQ-LIMIT-4 bad leaves, records or times throw a code, and the check reads nothing but its arguments
---
duration_ms: 2.155059
type: 'test'
...
# Subtest: REQ-LIMIT-2 no daily quotas: every launch is allowed and the report is empty
ok 26 - REQ-LIMIT-2 no daily quotas: every launch is allowed and the report is empty
---
duration_ms: 0.190579
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: plain
ok 27 - row 307 pi answers what row 270 read: plain
---
duration_ms: 2.273277
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: budget
ok 28 - row 307 pi answers what row 270 read: budget
---
duration_ms: 0.560346
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: cap
ok 29 - row 307 pi answers what row 270 read: cap
---
duration_ms: 0.447806
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: shrank
ok 30 - row 307 pi answers what row 270 read: shrank
---
duration_ms: 0.41824
type: 'test'
...
# Subtest: row 307 pi answers what row 270 read: header
ok 31 - row 307 pi answers what row 270 read: header
---
duration_ms: 0.525662
type: 'test'
...
# Subtest: row 307 pi's first collection over Claude history answers harness-unsupported without a scan, and a later one scans
ok 32 - row 307 pi's first collection over Claude history answers harness-unsupported without a scan, and a later one scans
---
duration_ms: 0.854402
type: 'test'
...
# Subtest: row 307 claude-code reads nothing and answers harness-unsupported with no version
ok 33 - row 307 claude-code reads nothing and answers harness-unsupported with no version
---
duration_ms: 0.145488
type: 'test'
...
# Subtest: row 307 a harness module without compactions doesn't load: missing-export
ok 34 - row 307 a harness module without compactions doesn't load: missing-export
---
duration_ms: 2.544641
type: 'test'
...
# Subtest: row 65 both harness modules load through the loader with every export the kind names
ok 35 - row 65 both harness modules load through the loader with every export the kind names
---
duration_ms: 7.887813
type: 'test'
...
# Subtest: row 65 each module answers for its own harness: adapter, levels, account, web, turn end and session files
ok 36 - row 65 each module answers for its own harness: adapter, levels, account, web, turn end and session files
---
duration_ms: 0.743006
type: 'test'
...
# Subtest: row 65 claude-code preflight: a missing credential, then a missing or relative binary, each refuse with their code
ok 37 - row 65 claude-code preflight: a missing credential, then a missing or relative binary, each refuse with their code
---
duration_ms: 3.607123
type: 'test'
...
# Subtest: row 65 claude-code version asks MOSAIC_CLAUDE_BIN when set, else claude on PATH, else null
ok 38 - row 65 claude-code version asks MOSAIC_CLAUDE_BIN when set, else claude on PATH, else null
---
duration_ms: 3.846673
type: 'test'
...
# Subtest: row 65 pi preflight passes with the repository's pi, and its version is the package pin
ok 39 - row 65 pi preflight passes with the repository's pi, and its version is the package pin
---
duration_ms: 8.397292
type: 'test'
...
# Subtest: row 65 env passes the thinking level to both adapters, and claude-code's bundle writes its hook and MCP files
ok 40 - row 65 env passes the thinking level to both adapters, and claude-code's bundle writes its hook and MCP files
---
duration_ms: 1.435832
type: 'test'
...
# Subtest: row 65 a route's thinking reaches pi as --thinking and claude as --effort, through each module's env and adapter
ok 41 - row 65 a route's thinking reaches pi as --thinking and claude as --effort, through each module's env and adapter
---
duration_ms: 15.483026
type: 'test'
...
# Subtest: row 65 no core source outside packages/plugins names a harness, apart from the listed exceptions
ok 42 - row 65 no core source outside packages/plugins names a harness, apart from the listed exceptions
---
duration_ms: 14.005572
type: 'test'
...
# Subtest: row 65 the grep's pattern catches a quoted name and a regex alternation, and not a word in prose
ok 43 - row 65 the grep's pattern catches a quoted name and a regex alternation, and not a word in prose
---
duration_ms: 0.162322
type: 'test'
...
# Subtest: REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source at interface version 1, limit-check at 2 (row 125), each a list
ok 44 - REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source at interface version 1, limit-check at 2 (row 125), each a list
---
duration_ms: 1.063022
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins accepts one key per kind, each a list of names, empty included
ok 45 - REQ-PLUG-1 checkPlugins accepts one key per kind, each a list of names, empty included
---
duration_ms: 0.24919
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates
ok 46 - REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates
---
duration_ms: 0.939348
type: 'test'
...
# Subtest: REQ-PLUG-1 a kind that switches on one module takes one name, not a list
ok 47 - REQ-PLUG-1 a kind that switches on one module takes one name, not a list
---
duration_ms: 0.155413
type: 'test'
...
# Subtest: REQ-PLUG-1 checkPlugins reports through the caller's fail callback
ok 48 - REQ-PLUG-1 checkPlugins reports through the caller's fail callback
---
duration_ms: 0.16264
type: 'test'
...
# Subtest: REQ-PLUG-1 fixture modules of each kind switch on from a test organization file, no core file changed
ok 49 - REQ-PLUG-1 fixture modules of each kind switch on from a test organization file, no core file changed
---
duration_ms: 9.287009
type: 'test'
...
# Subtest: REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting
ok 50 - REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting
---
duration_ms: 3.680508
type: 'test'
...
# Subtest: REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting
ok 51 - REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting
---
duration_ms: 3.446331
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting
ok 52 - REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting
---
duration_ms: 2.671649
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting
ok 53 - REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting
---
duration_ms: 2.37595
type: 'test'
...
# Subtest: REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it)
ok 54 - REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it)
---
duration_ms: 25.90956
type: 'test'
...
# Subtest: REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check
ok 55 - REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check
---
duration_ms: 2.906707
type: 'test'
...
# Subtest: REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting
ok 56 - REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting
---
duration_ms: 2.796915
type: 'test'
...
# Subtest: REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath
ok 57 - REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath
---
duration_ms: 2.400995
type: 'test'
...
# Subtest: REQ-PLUG-1 a module that fails to load refuses and keeps the old setting
ok 58 - REQ-PLUG-1 a module that fails to load refuses and keeps the old setting
---
duration_ms: 2.69425
type: 'test'
...
# Subtest: REQ-PLUG-1 a missing export refuses and keeps the old setting
ok 59 - REQ-PLUG-1 a missing export refuses and keeps the old setting
---
duration_ms: 3.312634
type: 'test'
...
# Subtest: REQ-PLUG-1 a different interface version refuses and keeps the old setting
ok 60 - REQ-PLUG-1 a different interface version refuses and keeps the old setting
---
duration_ms: 2.951412
type: 'test'
...
# Subtest: REQ-PLUG-1 REQ-LIMIT-3 a version 1 limit check refuses after the bump to 2, and the old setting stays
ok 61 - REQ-PLUG-1 REQ-LIMIT-3 a version 1 limit check refuses after the bump to 2, and the old setting stays
---
duration_ms: 2.585281
type: 'test'
...
# Subtest: REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name
ok 62 - REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name
---
duration_ms: 1.87625
type: 'test'
...
# Subtest: REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything
ok 63 - REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything
---
duration_ms: 2.675823
type: 'test'
...
# Subtest: REQ-PLUG-1 loads run one at a time, so the last requested setting wins
ok 64 - REQ-PLUG-1 loads run one at a time, so the last requested setting wins
---
duration_ms: 102.001355
type: 'test'
...
# Subtest: REQ-PLUG-1 the loader needs a log callback
ok 65 - REQ-PLUG-1 the loader needs a log callback
---
duration_ms: 1.130584
type: 'test'
...
# Subtest: C2-10c webui permits exactly the three added room verbs and preserves core refusals
ok 66 - C2-10c webui permits exactly the three added room verbs and preserves core refusals
---
duration_ms: 1.532674
type: 'test'
...
# Subtest: REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers
ok 67 - REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers
---
duration_ms: 2.616283
type: 'test'
...
# Subtest: REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws
ok 68 - REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws
---
duration_ms: 1.153042
type: 'test'
...
# Subtest: REQ-PLUG-2 search-source takes one name, not a list
ok 69 - REQ-PLUG-2 search-source takes one name, not a list
---
duration_ms: 0.232651
type: 'test'
...
# Subtest: REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng
ok 70 - REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng
---
duration_ms: 0.766974
type: 'test'
...
# Subtest: REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called
ok 71 - REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called
---
duration_ms: 0.633341
type: 'test'
...
# Subtest: the searxng modules end the request when the broker's AbortSignal fires
ok 72 - the searxng modules end the request when the broker's AbortSignal fires
---
duration_ms: 41.975824
type: 'test'
...
# Subtest: hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name
ok 73 - hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name
---
duration_ms: 0.572554
type: 'test'
...
# Subtest: REQ-LIMIT-3 token-limit exports interface version 2 with mayLaunch, mustStop and report
ok 74 - REQ-LIMIT-3 token-limit exports interface version 2 with mayLaunch, mustStop and report
---
duration_ms: 4.069294
type: 'test'
...
# Subtest: REQ-LIMIT-3 each subject kind refuses at its limit and allows under it
ok 75 - REQ-LIMIT-3 each subject kind refuses at its limit and allows under it
---
duration_ms: 1.264429
type: 'test'
...
# Subtest: REQ-LIMIT-3 a limit covers only its own subject's records and launches
ok 76 - REQ-LIMIT-3 a limit covers only its own subject's records and launches
---
duration_ms: 0.310153
type: 'test'
...
# Subtest: REQ-LIMIT-4 only known records count; an unknown turn adds nothing and is counted apart
ok 77 - REQ-LIMIT-4 only known records count; an unknown turn adds nothing and is counted apart
---
duration_ms: 0.245871
type: 'test'
...
# Subtest: REQ-LIMIT-3 a reset moves since: records before it no longer count
ok 78 - REQ-LIMIT-3 a reset moves since: records before it no longer count
---
duration_ms: 0.178373
type: 'test'
...
# Subtest: REQ-LIMIT-3 limit 0 refuses with no records; the first limit hit in file order is named
ok 79 - REQ-LIMIT-3 limit 0 refuses with no records; the first limit hit in file order is named
---
duration_ms: 0.159818
type: 'test'
...
# Subtest: REQ-LIMIT-3 a key limit is not enforced while every record's account is null (decision 150 Q1)
ok 80 - REQ-LIMIT-3 a key limit is not enforced while every record's account is null (decision 150 Q1)
---
duration_ms: 0.172512
type: 'test'
...
# Subtest: REQ-LIMIT-3 counts past the safe integer range come back as digit strings
ok 81 - REQ-LIMIT-3 counts past the safe integer range come back as digit strings
---
duration_ms: 0.145224
type: 'test'
...
# Subtest: REQ-LIMIT-4 bad records or times throw a code, and the check reads nothing but its arguments
ok 82 - REQ-LIMIT-4 bad records or times throw a code, and the check reads nothing but its arguments
---
duration_ms: 0.661745
type: 'test'
...
# Subtest: REQ-LIMIT-3 no token limits: every launch is allowed and the report is empty
ok 83 - REQ-LIMIT-3 no token limits: every launch is allowed and the report is empty
---
duration_ms: 0.624533
type: 'test'
...
# Subtest: REQ-LIMIT-3 token-limit ignores tokens.daily, which the daily-quota check enforces (row 126)
ok 84 - REQ-LIMIT-3 token-limit ignores tokens.daily, which the daily-quota check enforces (row 126)
---
duration_ms: 0.133942
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi: reports all token kinds and deduplicates assistant ids
ok 85 - REQ-LIMIT-4 pi: reports all token kinds and deduplicates assistant ids
---
duration_ms: 2.821247
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code: reports all token kinds and deduplicates assistant ids
ok 86 - REQ-LIMIT-4 claude-code: reports all token kinds and deduplicates assistant ids
---
duration_ms: 0.696333
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi cut line is unknown, never zero or content
ok 87 - REQ-LIMIT-4 pi cut line is unknown, never zero or content
---
duration_ms: 1.007977
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi invalid UTF-8 is unknown, never zero or content
ok 88 - REQ-LIMIT-4 pi invalid UTF-8 is unknown, never zero or content
---
duration_ms: 0.493409
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi missing input is unknown, never zero or content
ok 89 - REQ-LIMIT-4 pi missing input is unknown, never zero or content
---
duration_ms: 0.57636
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi negative count is unknown, never zero or content
ok 90 - REQ-LIMIT-4 pi negative count is unknown, never zero or content
---
duration_ms: 0.49758
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi fractional count is unknown, never zero or content
ok 91 - REQ-LIMIT-4 pi fractional count is unknown, never zero or content
---
duration_ms: 0.422118
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi unsafe integer is unknown, never zero or content
ok 92 - REQ-LIMIT-4 pi unsafe integer is unknown, never zero or content
---
duration_ms: 0.397082
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi file size is unknown, never zero or content
ok 93 - REQ-LIMIT-4 pi file size is unknown, never zero or content
---
duration_ms: 6.459547
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi line size is unknown, never zero or content
ok 94 - REQ-LIMIT-4 pi line size is unknown, never zero or content
---
duration_ms: 3.92269
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi symlink file is unknown, never zero or content
ok 95 - REQ-LIMIT-4 pi symlink file is unknown, never zero or content
---
duration_ms: 0.488148
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi hardlink file is unknown, never zero or content
ok 96 - REQ-LIMIT-4 pi hardlink file is unknown, never zero or content
---
duration_ms: 0.325503
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code cut line is unknown, never zero or content
ok 97 - REQ-LIMIT-4 claude-code cut line is unknown, never zero or content
---
duration_ms: 0.377051
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code invalid UTF-8 is unknown, never zero or content
ok 98 - REQ-LIMIT-4 claude-code invalid UTF-8 is unknown, never zero or content
---
duration_ms: 0.388899
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code missing input is unknown, never zero or content
ok 99 - REQ-LIMIT-4 claude-code missing input is unknown, never zero or content
---
duration_ms: 0.403653
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code negative count is unknown, never zero or content
ok 100 - REQ-LIMIT-4 claude-code negative count is unknown, never zero or content
---
duration_ms: 0.356921
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code fractional count is unknown, never zero or content
ok 101 - REQ-LIMIT-4 claude-code fractional count is unknown, never zero or content
---
duration_ms: 0.388944
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code unsafe integer is unknown, never zero or content
ok 102 - REQ-LIMIT-4 claude-code unsafe integer is unknown, never zero or content
---
duration_ms: 0.377099
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code file size is unknown, never zero or content
ok 103 - REQ-LIMIT-4 claude-code file size is unknown, never zero or content
---
duration_ms: 6.07976
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code line size is unknown, never zero or content
ok 104 - REQ-LIMIT-4 claude-code line size is unknown, never zero or content
---
duration_ms: 1.893556
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code symlink file is unknown, never zero or content
ok 105 - REQ-LIMIT-4 claude-code symlink file is unknown, never zero or content
---
duration_ms: 0.403314
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code hardlink file is unknown, never zero or content
ok 106 - REQ-LIMIT-4 claude-code hardlink file is unknown, never zero or content
---
duration_ms: 0.316423
type: 'test'
...
# Subtest: REQ-LIMIT-4 pi a symlink directory anywhere in the root refuses
ok 107 - REQ-LIMIT-4 pi a symlink directory anywhere in the root refuses
---
duration_ms: 0.322364
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code a symlink directory anywhere in the root refuses
ok 108 - REQ-LIMIT-4 claude-code a symlink directory anywhere in the root refuses
---
duration_ms: 0.311861
type: 'test'
...
# Subtest: REQ-LIMIT-4 bounded directory entries refuse before scanning an arbitrary tree
ok 109 - REQ-LIMIT-4 bounded directory entries refuse before scanning an arbitrary tree
---
duration_ms: 8.32832
type: 'test'
...
# Subtest: REQ-LIMIT-4 conflicting repeated usage refuses rather than double counting
ok 110 - REQ-LIMIT-4 conflicting repeated usage refuses rather than double counting
---
duration_ms: 0.528438
type: 'test'
...
# Subtest: REQ-LIMIT-4 oversized metadata refuses before reading any transcript bytes
ok 111 - REQ-LIMIT-4 oversized metadata refuses before reading any transcript bytes
---
duration_ms: 5.788996
type: 'test'
...
# Subtest: REQ-LIMIT-4 a file growing after fstat cannot exceed the streaming byte budget
ok 112 - REQ-LIMIT-4 a file growing after fstat cannot exceed the streaming byte budget
---
duration_ms: 4.822522
type: 'test'
...
# Subtest: REQ-LIMIT-4 a same-size edit during a read is unknown rather than a stable snapshot
ok 113 - REQ-LIMIT-4 a same-size edit during a read is unknown rather than a stable snapshot
---
duration_ms: 0.77981
type: 'test'
...
# Subtest: REQ-LIMIT-4 record count is bounded even when each record is tiny
ok 114 - REQ-LIMIT-4 record count is bounded even when each record is tiny
---
duration_ms: 7.87409
type: 'test'
...
# Subtest: REQ-LIMIT-4 missing assistant usage, malformed JSON and conflicting identities are unknown
ok 115 - REQ-LIMIT-4 missing assistant usage, malformed JSON and conflicting identities are unknown
---
duration_ms: 0.831847
type: 'test'
...
# Subtest: REQ-LIMIT-4 caller roots, unsupported harnesses and non-object JSON never become zero usage
ok 116 - REQ-LIMIT-4 caller roots, unsupported harnesses and non-object JSON never become zero usage
---
duration_ms: 0.640367
type: 'test'
...
# Subtest: REQ-LIMIT-4 even a complete JSON record with no final newline is an unknown cut line
ok 117 - REQ-LIMIT-4 even a complete JSON record with no final newline is an unknown cut line
---
duration_ms: 0.331929
type: 'test'
...
# Subtest: REQ-LIMIT-4 final file opens are nonblocking before hostile file type is known
ok 118 - REQ-LIMIT-4 final file opens are nonblocking before hostile file type is known
---
duration_ms: 0.469933
type: 'test'
...
# Subtest: REQ-LIMIT-4 invalid transcript entry names refuse before file lookup
ok 119 - REQ-LIMIT-4 invalid transcript entry names refuse before file lookup
---
duration_ms: 0.279053
type: 'test'
...
# Subtest: REQ-LIMIT-4 claude-code refuses a symlinked project directory
ok 120 - REQ-LIMIT-4 claude-code refuses a symlinked project directory
---
duration_ms: 0.491358
type: 'test'
...
1..120
# tests 120
# suites 0
# pass 120
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 829.707835
@@ -0,0 +1,5 @@
{
"worktree": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"nodeChromiumRemaining": [],
"method": "Own cwd or explicit worktree command path; fresh child groups killed after every gate/mutant run. No other seat signalled."
}
@@ -0,0 +1,320 @@
[
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/bus-full.tap",
"display": "bus-full.tap",
"rawSHA256": "cecdb996f0d0f34b592bdc1d8f7015cf1fcef48b7235f34237fb7edf8c194d5c",
"displaySHA256": "21e5b1cecc5a1499a7ae12b73fef50adaad8ada5f6ad8c222ed098e0add2fe0c"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/editor-build.tap",
"display": "editor-build.tap",
"rawSHA256": "6e4fb4cbdd36ff817c8246e727f382ada773d4667f314e0a5a187369c2c994ba",
"displaySHA256": "6e4fb4cbdd36ff817c8246e727f382ada773d4667f314e0a5a187369c2c994ba"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/plugin-green.tap",
"display": "plugin-green.tap",
"rawSHA256": "812b4fe16ba42152021741cd518913c68a834afb2b41da2a704c9f1eea87ba3e",
"displaySHA256": "812b4fe16ba42152021741cd518913c68a834afb2b41da2a704c9f1eea87ba3e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/plugin-red.tap",
"display": "plugin-red.tap",
"rawSHA256": "a03dbb651717487be38b57bd793727e5627f37c8d3efd19483dbe979d2c7db46",
"displaySHA256": "a03dbb651717487be38b57bd793727e5627f37c8d3efd19483dbe979d2c7db46"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/plugins-full.tap",
"display": "plugins-full.tap",
"rawSHA256": "287e1f34ebab1c9290d7553ac4ba5319bf6c660ce26b430b2c69535dd803013c",
"displaySHA256": "287e1f34ebab1c9290d7553ac4ba5319bf6c660ce26b430b2c69535dd803013c"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-browser-final.tap",
"display": "room-browser-final.tap",
"rawSHA256": "9061501c7cc991c2710df07f1db7f564dc2bf6119de11ec9835ebd23e62f9137",
"displaySHA256": "9061501c7cc991c2710df07f1db7f564dc2bf6119de11ec9835ebd23e62f9137"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-browser-final2.tap",
"display": "room-browser-final2.tap",
"rawSHA256": "e20f05a46438679273ab94a3bf8e718421e647c38068283bccb6895a5e2938c3",
"displaySHA256": "e20f05a46438679273ab94a3bf8e718421e647c38068283bccb6895a5e2938c3"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-browser-green.tap",
"display": "room-browser-green.tap",
"rawSHA256": "a5197d7417efef5ba7dfa9c480516e26719e366adaeaeef49cbcfa9ae96285ee",
"displaySHA256": "a5197d7417efef5ba7dfa9c480516e26719e366adaeaeef49cbcfa9ae96285ee"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-browser-green2.tap",
"display": "room-browser-green2.tap",
"rawSHA256": "d00a8f2842a74adbd304020dd4536bd0328a8fe50d10462267673bb87b93a69e",
"displaySHA256": "d00a8f2842a74adbd304020dd4536bd0328a8fe50d10462267673bb87b93a69e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-browser-red.tap",
"display": "room-browser-red.tap",
"rawSHA256": "0049cf34649eb250262a365dfa65f579f02d50cc1df4c0139c3e2c17c37d91a5",
"displaySHA256": "0049cf34649eb250262a365dfa65f579f02d50cc1df4c0139c3e2c17c37d91a5"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-check-green.tap",
"display": "room-check-green.tap",
"rawSHA256": "efd5ac58b3cc94f4b5d831b5372c96cf1390af2eaf4215a55c179b97ad72bb64",
"displaySHA256": "efd5ac58b3cc94f4b5d831b5372c96cf1390af2eaf4215a55c179b97ad72bb64"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-check-red.tap",
"display": "room-check-red.tap",
"rawSHA256": "b2917225a113bdf5a9cc2ff9efceb336f0cb8cbc4593bf8b4b2aa2292b025a9d",
"displaySHA256": "c41c959a330ff01db5941da91ee64e5ac398850710127920809ec0368b32026c"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-http-final.tap",
"display": "room-http-final.tap",
"rawSHA256": "25844a7bcca478617d95ec23460dd58aca539232d2eb3047d6537b5b4b5f3261",
"displaySHA256": "25844a7bcca478617d95ec23460dd58aca539232d2eb3047d6537b5b4b5f3261"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-http-green.tap",
"display": "room-http-green.tap",
"rawSHA256": "d2a309b9e9b9859f4c4ba4352cc70ecf70cdfded31c187b8361250071b508140",
"displaySHA256": "d2a309b9e9b9859f4c4ba4352cc70ecf70cdfded31c187b8361250071b508140"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-http-red.tap",
"display": "room-http-red.tap",
"rawSHA256": "9d642f087190461f71d26d8a2e5802ed329f9499a07eafa763eacc1b1a7cde6e",
"displaySHA256": "2c27ec9c7ee06484440705b2166f95042c8db357a2879930f55b68fb62557f72"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/room-http-validation-red.tap",
"display": "room-http-validation-red.tap",
"rawSHA256": "12ebb7b83c3f59bfefc5ea3964f40a868b2405661533a74f01644486baf6fe12",
"displaySHA256": "fbddc50f3cf24c42520b4b7fd7649c83f52409346cc177ddd946286ac712cc02"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/t9-rerun-1.tap",
"display": "t9-rerun-1.tap",
"rawSHA256": "221bae8b7e9e8dee198580e8c3985dc7cef88f2b049b2c2f536df67cbb0d7ac9",
"displaySHA256": "221bae8b7e9e8dee198580e8c3985dc7cef88f2b049b2c2f536df67cbb0d7ac9"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/t9-rerun-2.tap",
"display": "t9-rerun-2.tap",
"rawSHA256": "13cc40f64acd8311cc48d695a8f97c277ca115bf7e5dc72dbe3696b8144fc8be",
"displaySHA256": "13cc40f64acd8311cc48d695a8f97c277ca115bf7e5dc72dbe3696b8144fc8be"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/t9-rerun-3.tap",
"display": "t9-rerun-3.tap",
"rawSHA256": "d398bd81d1f86a599bd8da59d5d45c6a534d75f656046880042d687ca1b2f07f",
"displaySHA256": "d398bd81d1f86a599bd8da59d5d45c6a534d75f656046880042d687ca1b2f07f"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-auth.tap",
"display": "test-auth.tap",
"rawSHA256": "f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77",
"displaySHA256": "f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-conductor.tap",
"display": "test-conductor.tap",
"rawSHA256": "c50ff64b098027711e20445f4c02612a9a2db0c316b17160811f8beadf183ad6",
"displaySHA256": "c50ff64b098027711e20445f4c02612a9a2db0c316b17160811f8beadf183ad6"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-config.tap",
"display": "test-config.tap",
"rawSHA256": "92ea8ce5e66d892fefacc76b87d140bcfb8c5a51dc77bdf4a47a6213ac5d2898",
"displaySHA256": "92ea8ce5e66d892fefacc76b87d140bcfb8c5a51dc77bdf4a47a6213ac5d2898"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-discord.tap",
"display": "test-discord.tap",
"rawSHA256": "9b482b7a3f0672c872923c06e71d5a88ff085a08170905a6cabe1d2b6fc283b6",
"displaySHA256": "9b482b7a3f0672c872923c06e71d5a88ff085a08170905a6cabe1d2b6fc283b6"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-extension-package.tap",
"display": "test-extension-package.tap",
"rawSHA256": "3aae7d4f584eb86c19a87fc2589ede0159617c1cc59d35e1e326ebf1830a4bfc",
"displaySHA256": "3aae7d4f584eb86c19a87fc2589ede0159617c1cc59d35e1e326ebf1830a4bfc"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-federation-acceptance.tap",
"display": "test-federation-acceptance.tap",
"rawSHA256": "674d778090229d0d9adcad089e8cbeaf811c5e797380655a57dce37ca7fc768e",
"displaySHA256": "674d778090229d0d9adcad089e8cbeaf811c5e797380655a57dce37ca7fc768e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-foundation.tap",
"display": "test-foundation.tap",
"rawSHA256": "83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f",
"displaySHA256": "83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-land-work.tap",
"display": "test-land-work.tap",
"rawSHA256": "3e2f8024027592e77790db2bbe039803060915ebf37c7b67f04071a7e83d20dc",
"displaySHA256": "3e2f8024027592e77790db2bbe039803060915ebf37c7b67f04071a7e83d20dc"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-push-check.tap",
"display": "test-push-check.tap",
"rawSHA256": "5d9a170308927264d212e3dd00e87463a21d55811cf9b4cb51dce74d6a10fea5",
"displaySHA256": "5d9a170308927264d212e3dd00e87463a21d55811cf9b4cb51dce74d6a10fea5"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-queue.tap",
"display": "test-queue.tap",
"rawSHA256": "ac1a44f25384f67386ebc0864b4d8c853bb3991eb5c75c5fc9b882841797850a",
"displaySHA256": "ac1a44f25384f67386ebc0864b4d8c853bb3991eb5c75c5fc9b882841797850a"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-release.tap",
"display": "test-release.tap",
"rawSHA256": "9f9dd11e2631a24ad55aaf7becc61965c592b00da08a4e72b98e0384d3a13ec7",
"displaySHA256": "9f9dd11e2631a24ad55aaf7becc61965c592b00da08a4e72b98e0384d3a13ec7"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/test-task.tap",
"display": "test-task.tap",
"rawSHA256": "a065947f07d70a4721b423da597968630bc8ec1659db14681af42bb3f974077d",
"displaySHA256": "a065947f07d70a4721b423da597968630bc8ec1659db14681af42bb3f974077d"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/webui-full.tap",
"display": "webui-full.tap",
"rawSHA256": "42fc6cc8d2d90ff8730a150942a16a12b3e1777c8b95c966a045c27c88c7777e",
"displaySHA256": "42fc6cc8d2d90ff8730a150942a16a12b3e1777c8b95c966a045c27c88c7777e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/bus-rerun.tap",
"display": "bus-rerun.tap",
"rawSHA256": "3a1901f3e1b7f80a0faa72d07b156bd9b8480a3d5e16f74d0d76401e2f26d456",
"displaySHA256": "3a1901f3e1b7f80a0faa72d07b156bd9b8480a3d5e16f74d0d76401e2f26d456"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/plugins-final.tap",
"display": "plugins-final.tap",
"rawSHA256": "06b737eefa512acf4256cd66d800eec63249148920242581a42921ab3f6fdf0e",
"displaySHA256": "06b737eefa512acf4256cd66d800eec63249148920242581a42921ab3f6fdf0e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r1/M01-plugin.tap",
"display": "mutants-r1/M01-plugin.tap",
"rawSHA256": "eb0f00b83a42cbaa3e277859d03521f3f499163e2eee602753100b0cae6424d4",
"displaySHA256": "eb0f00b83a42cbaa3e277859d03521f3f499163e2eee602753100b0cae6424d4"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r1/summary.json",
"display": "mutants-r1/summary.json",
"rawSHA256": "6cdb1522e895434b657534e400cbd38b0077e7db206fad61e45697731f941b7e",
"displaySHA256": "6cdb1522e895434b657534e400cbd38b0077e7db206fad61e45697731f941b7e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M01-plugin.tap",
"display": "mutants-r2/M01-plugin.tap",
"rawSHA256": "00f4f83caa6119b8189b757c8e21376a17b647fd0c691ad3a10bc4b26c4c13c1",
"displaySHA256": "00f4f83caa6119b8189b757c8e21376a17b647fd0c691ad3a10bc4b26c4c13c1"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M02-seam-body.tap",
"display": "mutants-r2/M02-seam-body.tap",
"rawSHA256": "17594067e463a4b0303adda4a8bc6476b4bae4e650f689d3b51714513e81adae",
"displaySHA256": "35b5d70ed2cf81dc9deb6610c8ffd4a018229ea98e0e8d4621f93049b580d40d"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M03-page-cursor.tap",
"display": "mutants-r2/M03-page-cursor.tap",
"rawSHA256": "6e8cf3a24163cb20c17a8a0072c96e753f5a2514016a15234502eabb2f93e3e0",
"displaySHA256": "43081c6826c91d0b75907d41426ca291bcf174a0058dad9013df66b1121272b5"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M04-room-status.tap",
"display": "mutants-r2/M04-room-status.tap",
"rawSHA256": "5f2fa244ddf7afc83157469a66f95aea943ae0bcbe93cdb5e4a70200399dc2bd",
"displaySHA256": "c6cce27974bd6ee0fc40b49245fefd7e9534599a3f24372af6c083aaeffb5b60"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M05-strict-body.tap",
"display": "mutants-r2/M05-strict-body.tap",
"rawSHA256": "b8bc48c7a52a98ec8b67fb3c53e05fb1f59e078f38ff657742548a2ea4d3c2ef",
"displaySHA256": "aca9475140979a3768b7e4782b216960f4f5f780e8ca4beeb588af8a565c643e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M06-receipt-type.tap",
"display": "mutants-r2/M06-receipt-type.tap",
"rawSHA256": "8f78507bc39046d4b49120d51d8ca02070ba2e2914375b3592f799fe8afe9a62",
"displaySHA256": "127bfe8686dde4d1c1167d3a94c39ad54b65c971efc293bcd255b4e35f169195"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M07-room-check.tap",
"display": "mutants-r2/M07-room-check.tap",
"rawSHA256": "e6aa52760107dfed980d1a1e78335e4d10e4dce677b9a0ea8e7dfcacf180b75e",
"displaySHA256": "f63ae69e63abe634db1a7856ed9a1586fe8836337324294f2adc64e9108ba10c"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M08-room-last.tap",
"display": "mutants-r2/M08-room-last.tap",
"rawSHA256": "9995569d51a4ec69b6017d9684eed630d757135524ace8f1a2b7c6193b398701",
"displaySHA256": "57ce0841659ad261ab255b10ce9c5b7efd566fb04ef2173537835ece01eb4107"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M09-browser-body.tap",
"display": "mutants-r2/M09-browser-body.tap",
"rawSHA256": "719941ff281651a629d0b440569bf32b9481a89489e5a1ce7b44cbb850269b83",
"displaySHA256": "dba34fe54be4f4693d6ba951d4c94fa5b1546da5a0c0a806aa0dced69d466cce"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M10-members.tap",
"display": "mutants-r2/M10-members.tap",
"rawSHA256": "27ccf704537c4c485d7bf8a575b8d92c5540495c8c5e8a6b61b68e1f6130982b",
"displaySHA256": "99d7d38a1e837595e17f8f071c0a96b7e49675e8a69d59c0ba9133fbdb2e2a8e"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M11-checked-render.tap",
"display": "mutants-r2/M11-checked-render.tap",
"rawSHA256": "50d9fc3c5f93784cbde1b7de2367d2e96ee08a8dd1acbd7c9555c57e3542ce43",
"displaySHA256": "2626e9f9bbc5c140c6797dc5f64c12bb25eebfec63e431355dfc382f724150d1"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M12-leading-tags.tap",
"display": "mutants-r2/M12-leading-tags.tap",
"rawSHA256": "9ce6330372a03847ecc2d6c43ecbb66765a94e211b55b1eac77b4306be3c7e18",
"displaySHA256": "08bac05bbbd5f6e4cf45efdb52680ea341a01b574e51e5ec75a482bc690d51a6"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M13-readonly.tap",
"display": "mutants-r2/M13-readonly.tap",
"rawSHA256": "f02435604b4a6f4d473e57aae13804c5cee23b1946628e06b22445895d5f9ceb",
"displaySHA256": "d4049ff617b6fdb0cafd13ff5925fb66a514082b2d051cdec60b7a05dcfe0b35"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M14-uncertainty.tap",
"display": "mutants-r2/M14-uncertainty.tap",
"rawSHA256": "6d365419381849631d4b369c4bdb91c35a6a690ccbac25634d6b481391b8a7bb",
"displaySHA256": "376dd9deda7c6934822901fc6dabbd99da10f481539f2418e193b14ca9b3ac88"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M15-pending.tap",
"display": "mutants-r2/M15-pending.tap",
"rawSHA256": "004007418152ae17ff29c173216d352600c7442962694131fbb88c7da9c9622e",
"displaySHA256": "63cfbaf89dd5b62a95eb94f2a7c658b2db2ab03a4f5d4a79c06f70ca391d7072"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/M16-body-dedup.tap",
"display": "mutants-r2/M16-body-dedup.tap",
"rawSHA256": "0e381526dec7b042cbf61667d03cc4377be6f84343c3e99ba244438faa3a36be",
"displaySHA256": "85223d3266cb90b764c2276f02b7df6d67ccc4ea392b1766fea6bc9162132632"
},
{
"raw": "/mnt/storage/scratch/tmp/KV9rHb/mutants-r2/summary.json",
"display": "mutants-r2/summary.json",
"rawSHA256": "1e9b5da043b16589c08d8bde1f627fb15b06f2302d83f01626d637f63b6b2e46",
"displaySHA256": "1e9b5da043b16589c08d8bde1f627fb15b06f2302d83f01626d637f63b6b2e46"
}
]
@@ -0,0 +1,66 @@
[
{
"step": "t9-rerun-1",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/bus/tests/library-credential.test.mjs"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 120,
"exit": 0,
"seconds": 1.367,
"cleanupSignals": []
},
{
"step": "t9-rerun-2",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/bus/tests/library-credential.test.mjs"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 120,
"exit": 0,
"seconds": 1.367,
"cleanupSignals": []
},
{
"step": "t9-rerun-3",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/bus/tests/library-credential.test.mjs"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 120,
"exit": 0,
"seconds": 1.367,
"cleanupSignals": []
},
{
"step": "bus-rerun",
"command": [
"node",
"--test",
"--test-reporter=tap",
"--test-concurrency=1",
"--test-timeout=30000",
"packages/bus/tests/"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 142.919,
"cleanupSignals": []
}
]
@@ -0,0 +1,74 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 579.685618
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 590.781432
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
not ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 477.156559
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:103:1'
failureType: 'testCodeFailure'
error: 'Uncaught: SyntaxError: missing ) after argument list'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:122:16)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 446.373694
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 467.224249
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 429.347385
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 481.360271
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 431.26383
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 445.816871
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 4457.473253
@@ -0,0 +1,64 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 612.127424
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 523.14041
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 575.124776
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 503.401984
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 496.507579
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 461.20871
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 528.355937
type: 'test'
...
# Subtest: C2-10c quiet members complete in an empty room, and Escape closes suggestions
ok 8 - C2-10c quiet members complete in an empty room, and Escape closes suggestions
---
duration_ms: 557.487691
type: 'test'
...
# Subtest: C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
ok 9 - C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft
---
duration_ms: 497.125002
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 4864.325898
@@ -0,0 +1,72 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 478.675099
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 482.553437
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
not ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 3478.222635
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:101:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: window.__writes.length===1 && !document.querySelector("#chat-room-body").disabled: undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:113:3)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 469.757127
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
not ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 3452.43392
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:128:1'
failureType: 'testCodeFailure'
error: "Uncaught (in promise) Error: window.__writes.length===1 && !document.querySelector('#chat-room-body').disabled: undefined"
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:134:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 450.601494
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 505.231021
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 5
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 9422.414756
@@ -0,0 +1,52 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 461.849386
type: 'test'
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 457.623189
type: 'test'
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 458.150891
type: 'test'
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 608.340525
type: 'test'
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 497.894326
type: 'test'
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 460.087976
type: 'test'
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 600.261859
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 3647.620295
@@ -0,0 +1,131 @@
TAP version 13
# Subtest: C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
not ok 1 - C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery
---
duration_ms: 3628.090522
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:77:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:78:29)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c explicit project Open room calls only room.open before navigating
not ok 2 - C2-10c explicit project Open room calls only room.open before navigating
---
duration_ms: 429.638963
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:94:1'
failureType: 'testCodeFailure'
error: |-
Uncaught: TypeError: Cannot read properties of null (reading 'click')
at <anonymous>:1:42
code: 'ERR_TEST_FAILURE'
stack: |-
<anonymous>:1:42
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:97:3)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
not ok 3 - C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly
---
duration_ms: 3424.260935
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:101:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:102:28)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c browser leading tag reader matches every broker parseAddress vector
not ok 4 - C2-10c browser leading tag reader matches every broker parseAddress vector
---
duration_ms: 3423.200884
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:118:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:119:22)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room refusals show fixed lines, preserve drafts and never retry
not ok 5 - C2-10c room refusals show fixed lines, preserve drafts and never retry
---
duration_ms: 3450.058918
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:128:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:129:34)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c read-only room does not offer a send or an implicit open
not ok 6 - C2-10c read-only room does not offer a send or an implicit open
---
duration_ms: 3443.193406
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:142:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:143:22)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c late reads and sign-out cannot repaint private room state
not ok 7 - C2-10c late reads and sign-out cannot repaint private room state
---
duration_ms: 3457.523072
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:150:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelector(".chat-room"): undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async setup (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:74:3)
async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-browser.test.mjs:151:28)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
1..7
# tests 7
# suites 0
# pass 0
# fail 7
# cancelled 0
# skipped 0
# todo 0
# duration_ms 21368.466856
@@ -0,0 +1,22 @@
TAP version 13
# Subtest: C2-10c room.read carries exactly the thread check and current file last
ok 1 - C2-10c room.read carries exactly the thread check and current file last
---
duration_ms: 66.788688
type: 'test'
...
# Subtest: C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
ok 2 - C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
---
duration_ms: 14.902072
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 154.8326
@@ -0,0 +1,55 @@
TAP version 13
# Subtest: C2-10c room.read carries exactly the thread check and current file last
not ok 1 - C2-10c room.read carries exactly the thread check and current file last
---
duration_ms: 68.925074
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/bus/tests/room-check.test.mjs:10:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
+ undefined
- {
- citations: [
- {
- agent: null,
- file: 'e46ffb32a683035cf812aa05e1773ba95f2c9ee4',
- last: 'e46ffb32a683035cf812aa05e1773ba95f2c9ee4',
- scope: 'canon'
- }
- ]
- }
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
citations:
0:
scope: 'canon'
agent: ~
file: 'e46ffb32a683035cf812aa05e1773ba95f2c9ee4'
last: 'e46ffb32a683035cf812aa05e1773ba95f2c9ee4'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/bus/tests/room-check.test.mjs:20:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
ok 2 - C2-10c check hydration preserves unchecked rows and one broadcast with two addressees
---
duration_ms: 16.111117
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 1
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 159.952195
@@ -0,0 +1,52 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 10.41166
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 11.761026
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.734065
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 4.365649
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 2.682212
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 2.090283
type: 'test'
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.625202
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 144.565659
@@ -0,0 +1,40 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 9.140924
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 10.424582
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.318333
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 3.458143
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 3.395395
type: 'test'
...
1..5
# tests 5
# suites 0
# pass 5
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 135.694931
@@ -0,0 +1,127 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
not ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 8.731172
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:34:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ 'undefined'
- 'function'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 'function'
actual: 'undefined'
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:37:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
not ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 9.016525
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:44:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
404 !== 200
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 200
actual: 404
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:48:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c room write fences and strict keys reject before transport
not ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 3.141918
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:62:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
404 !== 400
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 400
actual: 404
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:66:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
not ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 1.798433
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:75:1'
failureType: 'testCodeFailure'
error: |-
unknown-addressee
404 !== 409
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 409
actual: 404
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:80:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
not ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 1.788338
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:85:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
404 !== 503
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 503
actual: 404
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:90:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
1..5
# tests 5
# suites 0
# pass 0
# fail 5
# cancelled 0
# skipped 0
# todo 0
# duration_ms 124.767975
@@ -0,0 +1,69 @@
TAP version 13
# Subtest: C2-10c seam exposes three fixed room calls without thread.messages fanout
ok 1 - C2-10c seam exposes three fixed room calls without thread.messages fanout
---
duration_ms: 9.95559
type: 'test'
...
# Subtest: C2-10c HTTP preserves original send, member receipt and the last grouped paging id
ok 2 - C2-10c HTTP preserves original send, member receipt and the last grouped paging id
---
duration_ms: 11.053371
type: 'test'
...
# Subtest: C2-10c room write fences and strict keys reject before transport
ok 3 - C2-10c room write fences and strict keys reject before transport
---
duration_ms: 9.160608
type: 'test'
...
# Subtest: C2-10c all four room refusals are409 with fixed text, never transport prose
ok 4 - C2-10c all four room refusals are409 with fixed text, never transport prose
---
duration_ms: 4.022295
type: 'test'
...
# Subtest: C2-10c malformed/lost write receipts are unknown and never retried
ok 5 - C2-10c malformed/lost write receipts are unknown and never retried
---
duration_ms: 2.737076
type: 'test'
...
# Subtest: C2-10c room receipts reject coerced member keys and duplicate grouped ids
not ok 6 - C2-10c room receipts reject coerced member keys and duplicate grouped ids
---
duration_ms: 1.75248
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:95:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
200 !== 503
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 503
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/packages/webui/tests/room-http.test.mjs:99:10)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: C2-10c granted-user room calls never borrow the owner transport
ok 7 - C2-10c granted-user room calls never borrow the owner transport
---
duration_ms: 0.878763
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 141.250822
@@ -0,0 +1,146 @@
[
{
"step": "test-auth",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-auth.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 0.866,
"cleanupSignals": []
},
{
"step": "test-conductor",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-conductor.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 7.53,
"cleanupSignals": []
},
{
"step": "test-config",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-config.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 2.72,
"cleanupSignals": []
},
{
"step": "test-discord",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-discord.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 8.131,
"cleanupSignals": []
},
{
"step": "test-extension-package",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-extension-package.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 1.467,
"cleanupSignals": []
},
{
"step": "test-federation-acceptance",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-federation-acceptance.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 3.972,
"cleanupSignals": []
},
{
"step": "test-foundation",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-foundation.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 38.592,
"cleanupSignals": []
},
{
"step": "test-land-work",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-land-work.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 12.039,
"cleanupSignals": []
},
{
"step": "test-push-check",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-push-check.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 38.944,
"cleanupSignals": []
},
{
"step": "test-queue",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-queue.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 21.708,
"cleanupSignals": []
},
{
"step": "test-release",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-release.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 2.92,
"cleanupSignals": []
},
{
"step": "test-task",
"command": [
"bash",
"/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-task.sh"
],
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266",
"outerSeconds": 1200,
"exit": 0,
"seconds": 13.793,
"cleanupSignals": []
}
]
@@ -0,0 +1,46 @@
TAP version 13
# Subtest: C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
ok 1 - C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
---
duration_ms: 27.654269
type: 'test'
...
# Subtest: C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
ok 2 - C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
---
duration_ms: 293.65964
type: 'test'
...
# Subtest: C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
ok 3 - C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
---
duration_ms: 34.60293
type: 'test'
...
# Subtest: C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
ok 4 - C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
---
duration_ms: 30.656128
type: 'test'
...
# Subtest: C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
ok 5 - C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
---
duration_ms: 16.276807
type: 'test'
...
# Subtest: C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
ok 6 - C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
---
duration_ms: 789.168747
type: 'test'
...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1315.368631
@@ -0,0 +1,46 @@
TAP version 13
# Subtest: C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
ok 1 - C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
---
duration_ms: 27.978617
type: 'test'
...
# Subtest: C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
ok 2 - C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
---
duration_ms: 304.610774
type: 'test'
...
# Subtest: C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
ok 3 - C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
---
duration_ms: 33.464243
type: 'test'
...
# Subtest: C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
ok 4 - C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
---
duration_ms: 30.002925
type: 'test'
...
# Subtest: C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
ok 5 - C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
---
duration_ms: 16.524439
type: 'test'
...
# Subtest: C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
ok 6 - C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
---
duration_ms: 754.840215
type: 'test'
...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1293.310491
@@ -0,0 +1,46 @@
TAP version 13
# Subtest: C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
ok 1 - C2-03b T6: the slot before any chief evidence, then the listed chief's live grant
---
duration_ms: 34.452051
type: 'test'
...
# Subtest: C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
ok 2 - C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended
---
duration_ms: 299.162474
type: 'test'
...
# Subtest: C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
ok 3 - C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again
---
duration_ms: 35.484814
type: 'test'
...
# Subtest: C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
ok 4 - C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant
---
duration_ms: 31.417755
type: 'test'
...
# Subtest: C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
ok 5 - C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses
---
duration_ms: 16.905839
type: 'test'
...
# Subtest: C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
ok 6 - C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000
---
duration_ms: 766.473551
type: 'test'
...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1302.772185
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,23 @@
On branch fethry/row266
Your branch is up to date with 'origin/fethry/row266'.
nothing to commit, working tree clean
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,57 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK valid console.sso loads and is in the output
OK console.sso HS256 alg exits 2 naming the path (exit 2)
OK console.sso alg none exits 2 naming the path (exit 2)
OK console.sso missing slot field exits 2 naming the path (exit 2)
OK console.sso slot name outside the pattern exits 2 naming the path (exit 2)
OK unknown key under console exits 2 naming the path (exit 2)
OK valid federation key loads and is in the output
OK federation unknown sub-key exits 2 naming the key (exit 2)
OK federation peer unknown sub-key exits 2 naming the key (exit 2)
OK federation peer origin over http exits 2 naming the key (exit 2)
OK federation peer origin with a path exits 2 naming the key (exit 2)
OK federation peer origin with userinfo exits 2 naming the key (exit 2)
OK federation peer id equal to self exits 2 naming the key (exit 2)
OK federation peer label empty exits 2 naming the key (exit 2)
OK federation not an object exits 2 naming the key (exit 2)
OK federation self pattern exits 2 naming the key (exit 2)
OK federation peers not an array exits 2 naming the key (exit 2)
OK federation peer not an object exits 2 naming the key (exit 2)
OK federation peer id pattern exits 2 naming the key (exit 2)
OK federation peer label over 80 characters exits 2 naming the key (exit 2)
OK federation duplicate peer id exits 2 naming the key (exit 2)
OK federation peer origin with a trailing slash exits 2 naming the key (exit 2)
OK federation self missing exits 2 naming the key (exit 2)
OK federation peer id missing exits 2 naming the key (exit 2)
OK federation peer label missing exits 2 naming the key (exit 2)
OK config without federation loads and has no federation output
OK config without console loads with unchanged output
OK compactionReadBytes 8388608 loads and is in the output as written
OK compactionReadBytes "x" loads and is in the output as written
OK compactionStreams 2048 loads and is in the output as written
OK compactionStreams "x" loads and is in the output as written
OK failed validation modified nothing
selftest: 55 passed, 0 failed
@@ -0,0 +1,86 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/processing.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/copy-service.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/library-seat.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/offline-check.test.mjs
OK syntax: packages/discord/tests/processing-stop.test.mjs
OK syntax: packages/discord/tests/processing.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 200)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
OK pinned render runs copy CLI with copy cwd/repo and no checkout executable
OK pinned install writes the rendered unit in a temporary unit directory
OK pinned install refuses short prerequisite and writes no unit
OK pinned install refuses missing prerequisite and writes no unit
OK pinned install refuses cli prerequisite and writes no unit
OK pinned install refuses pi prerequisite and writes no unit
OK status names the copy from fake systemctl effective cwd
OK checkout install explicitly recommends --copy
OK wrapper check --offline passes local fixture and makes no network call
OK wrapper run --offline refuses usage
discord suite: 82 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266/scripts/test-extension-package.sh: line 14: 2236844 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,10 @@
✔ FED R7 scratch two-instance acceptance (3582.728896ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3676.59229
R7 cleanup: no child groups, ports or sockets remain; no units created
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,88 @@
OK args: exit 2, target-branch
OK args: exit 2, target-branch
OK args: exit 4
OK args: exit 4
OK args: exit 4
OK --base next refuses before the config is read (got 2)
OK a missing system config exits 3 (got 3)
OK nobus: exit 3
OK schema-table: exit 2, bus-schema
OK schema-meta: exit 2, bus-schema
OK openBus opens the database read-only
OK other-business: exit 2, decision-not-found
OK no-raise: exit 2, decision-not-found
OK not-a-merge: exit 2, not-a-merge
OK open: exit 2, not-resolved
OK withdrawn: exit 2, not-resolved
OK by-agent: exit 2, not-human
OK via-webui: exit 2, not-human
OK no-answer: exit 2, not-human
OK answer-role: exit 2, not-human
OK answer-run: exit 2, not-human
OK answer-choice: exit 2, not-human
OK two-answers: exit 2, not-human
OK arbiter-cross: exit 2, not-human
OK arbiter-cited: exit 2, not-human
OK chose-no: exit 2, not-merge-option
OK two-refs: exit 2, work-ref
OK bad-ref: exit 2, work-ref
OK task-ref-disagrees: exit 2, work-ref
OK raise-task-disagrees: exit 2, work-ref
OK target-main: exit 2, target-branch
OK superseded-question: exit 2, superseded
OK superseded-task: exit 2, superseded
OK superseded-by: exit 2, superseded
OK no-verdict: exit 2, no-verdict
OK changes-later: exit 2, not-approved
OK short-commit: exit 2, not-approved
OK wrong-task-body: exit 2, not-approved
OK args: exit 2, decision-not-found
OK earlier-merge: exit 0
OK ref-moved: exit 2, ref-moved
OK root-top: exit 2, root-file
OK root-newdir: exit 2, root-file
OK root-src: exit 2, root-file
OK unrelated: exit 2, base-conflict
OK empty: exit 2, base-conflict
OK an empty reviewed diff says so
OK dirty-changed: exit 2, checkout-dirty
OK dirty-staged: exit 2, checkout-dirty
OK dirty-untracked: exit 2, checkout-dirty
OK conflict: exit 2, base-conflict
OK a refusal after the worktree is added leaves no worktree or scratch
OK cleanup looks for the worktree's Docker network
OK dirty-during: exit 2, checkout-dirty
OK a reviewed path dirtied during the gate moves no ref
OK drift: exit 2, patch-drift
OK patch-drift leaves no worktree or scratch
OK gate-pkg: exit 2, gate-failed
OK gate-failed names the failing step's log, and it exists
OK gate-failed leaves no worktree or scratch
OK gate-script: exit 2, gate-failed
OK a failing scripts/test-*.sh fails the gate
OK a failed gate moves no ref
OK nomods: exit 4
OK a target without node_modules leaves no worktree or scratch
OK moved: exit 2, base-moved
OK base-moved keeps the base where it moved to
OK dry: exit 0
OK --dry-run writes no ref
OK --dry-run prints the commit with --subject and --attribution
OK --dry-run runs the gate
OK --dry-run leaves no worktree or scratch
OK land: exit 0
OK the landing is one commit on the old base
OK its diff is the reviewed diff by patch-id
OK its tree is the dry run's
OK the message has the option's subject, the citations and the attribution last
OK it prints the line for the PM
OK the checked-out base has the reviewed paths and keeps the unrelated edit
OK only refs/heads/refactor exists in the target
OK the work ref is unchanged
OK the bus database is unchanged
OK the gate ran each package suite but queue, and each scripts/test-*.sh
OK the landing leaves no worktree or scratch
OK the target's node_modules is still there, with its contents
OK the worktree's Docker network is removed
---
land-work selftests: 86 passed, 0 failed
@@ -0,0 +1,7 @@
PASS push-check 38 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
KILLED skip-nul-scan
@@ -0,0 +1,36 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/gate-owner.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 156
ℹ pass 156
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row266) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 28 passed, 0 failed
@@ -0,0 +1,17 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
OK compose network made under the run's project and removed after the run
selftest: 15 passed, 0 failed
@@ -0,0 +1,97 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
OK mock adapter: gate passes on matching mock response (exit 0)
OK mock adapter: expect-mismatch recorded (exit 1)
OK mismatch reason recorded
OK unknown adapter fails closed (exit 1)
OK mission task runs via mock adapter (exit 0)
OK mission section injected into generated prompt
OK retry of mission run succeeds (exit 0)
OK retriedFrom lineage recorded
OK mission section present after retry (relative path resolved)
OK retry of missing run exits 4 (exit 4)
OK skill install bundled ms-tools (exit 0)
OK installed to skills-available
OK double install refuses (exit 1)
OK activate enables skill (exit 0)
OK enabled dir populated
OK uninstall while enabled refuses (exit 1)
OK deactivate moves back to available (exit 0)
OK uninstall removes from available (exit 0)
OK seat with enabled skill launches
OK skill path delivered to adapter
OK shipped researcher contract resolves (ceiling + network)
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
OK resolve-role refuses name/filename mismatch (exit 2)
OK resolve-role refuses unknown network declaration (exit 2)
OK resolve-role refuses duplicate role tool (exit 2)
OK resolve-role refuses unsupported tool (exit 2)
OK resolve-role refuses missing contract file (exit 4)
OK version 1 role prints no contract line
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
OK resolve-role accepts a minimal version 2 role (exit 0)
OK resolve-role refuses an action outside the vocabulary (exit 2)
OK resolve-role refuses a gated-only action in a role file (exit 2)
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
OK resolve-role refuses a contract that is a symbolic link (exit 2)
OK seat launches under role ceiling
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
OK narrowing recorded loudly
OK missing role contract refuses launch (exit 2)
OK missing-contract refusal names the role
OK empty ceiling intersection -> tool-free seat
OK tool-free outcome recorded loudly
OK overridden agents dir without seat definition refuses (exit 4)
OK seat-resolution refusal names the seat
OK policy: mission only -> mission tools
OK policy: task only -> task tools
OK policy: both -> intersection (task narrowed)
OK policy: empty intersection -> tool-free
OK invalid mission capabilities rejected (exit 2)
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
OK task run with user layer present (exit 0)
OK user context dispatched into generated prompt
OK workspace+tools task runs via mock (exit 0)
OK workspace path + tools delivered to adapter
OK persistent workspace created on host
OK plain task still runs (no workspace/tools) (exit 0)
OK workspace var present but empty when absent
OK tools empty when absent
OK unknown tool exits 2 (exit 2)
OK workspace traversal exits 2 (exit 2)
OK hello task succeeds with exact marker
OK result.json written in run dir
OK result.json contents are correct
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
OK each run gets a distinct run dir (no clobber)
OK list shows both runs
skip live provider cases (row 244: no provider call in the gate)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
skip live provider cases (row 244: no provider call in the gate)
OK no agent identity on headless run
OK compose network made under the run's project and removed after the run
selftest: 92 passed, 0 failed
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
db30ea0f64bd72a5e263d8ea699d5ae561e3c2f6d3f9d1e80a035823078e1531 packages/bus/src/broker.mjs
dac7cdd892b9965c64b16d138a49e4a0c4f5fd2189359780daf97ab7271c1938 packages/bus/tests/room-check.test.mjs
789132601745a94027f9e9184e7fef1f70cff0cba450afed5cca647f683591f4 packages/plugins/channel/webui/index.mjs
ea7fc5cb5bba4569a733f30bc185f81f917a0405b22e106cc3e03120a2322388 packages/plugins/tests/room-channel.test.mjs
3a5321a5f492a98d342e4ddcc1e9da3522cfff89a4f9bc3f51d2ab1426d3de1b packages/webui/src/chat-http.mjs
fe2072a7198771a82f58dca5fc05aa4d697dd3db25a206fb2a24c8514010b069 packages/webui/src/chat.mjs
c02235d52dc4884a8d49b279b1724274627ad027304c255ebaebd0bc198788a4 packages/webui/src/public/chat.css
abfed92be8c57024ee590a7669cdc7cabfbfece88f92c953a3bef2bedba412dd packages/webui/src/public/chat.js
92c0e956ef076af21c986b52e6f461f711b0b0e9115956cb1bcbeac0942a153c packages/webui/tests/room-browser.test.mjs
5b79a7523f087bec91d5c8a31ad59d9b34b21716a7032e120a4fe8358c72b5ce packages/webui/tests/room-http.test.mjs
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Isolated room mutants. Own worktree dependencies, assertion kills only, killpg after each run."""
import argparse, hashlib, json, os, signal, subprocess, tarfile, tempfile, time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
UI = 'packages/webui/src/public/chat.js'
HTTP = 'packages/webui/src/chat-http.mjs'
BROKER = 'packages/bus/src/broker.mjs'
PLUGIN = 'packages/plugins/channel/webui/index.mjs'
SEAM = 'packages/webui/src/chat.mjs'
BROWSER_TEST = 'packages/webui/tests/room-browser.test.mjs'
HTTP_TEST = 'packages/webui/tests/room-http.test.mjs'
BUS_TEST = 'packages/bus/tests/room-check.test.mjs'
PLUGIN_TEST = 'packages/plugins/tests/room-channel.test.mjs'
MUTANTS = [
('M01-plugin', PLUGIN, ' "room.open",\n', '', PLUGIN_TEST),
('M02-seam-body', SEAM, "channel('room.send', { project, body })", "channel('room.send', { project, body: body.trim() })", HTTP_TEST),
('M03-page-cursor', HTTP, 'value.at(-1)?.ids.at(-1)', 'value.at(-1)?.id', HTTP_TEST),
('M04-room-status', HTTP, 'Object.keys(ROOM_LINES).map(code => [code, 409])', 'Object.keys(ROOM_LINES).map(code => [code, 400])', HTTP_TEST),
('M05-strict-body', HTTP, "!exact(v, ['body']) || ", '', HTTP_TEST),
('M06-receipt-type', HTTP, "typeof t.instance === 'string' && ", '', HTTP_TEST),
('M07-room-check', BROKER, 'return entries.map((m) => this.#withCheck(m));', 'return entries;', BUS_TEST),
('M08-room-last', BROKER, "['thread.messages', 'room.read'].includes(request.verb)", "['thread.messages'].includes(request.verb)", BUS_TEST),
('M09-browser-body', UI, 'request(`rooms/${c.id}/messages`, { body })', 'request(`rooms/${c.id}/messages`, { body: body.trim() })', BROWSER_TEST),
('M10-members', UI, 'catalog.rows.filter(m => threads.rows.some(t => t.project === id && t.instance === m.instance && t.name === m.instance))', 'catalog.rows', BROWSER_TEST),
('M11-checked-render', UI, 'messageBody({ ...m, from_role:', 'messageBody({ ...m, check: null, from_role:', BROWSER_TEST),
('M12-leading-tags', UI, 'let body = input.trimStart();', 'let body = input;', BROWSER_TEST),
('M13-readonly', UI, 'if (!roomOwner() || !c.canSend || !c.roomReady || out())', 'if (!roomOwner() || !c.roomReady || out())', BROWSER_TEST),
('M14-uncertainty', HTTP, 'write ? sendOutcome(err) :', "write ? { code: 'invalid-request', sent: false } :", HTTP_TEST),
('M15-pending', UI, ' if (s.pending) return;', '', BROWSER_TEST),
('M16-body-dedup', UI, 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.id, m]))', 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.body, m]))', BROWSER_TEST),
]
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
def main():
p=argparse.ArgumentParser(); p.add_argument('--output',type=Path,required=True);p.add_argument('--only');a=p.parse_args()
names=set(a.only.split(',')) if a.only else {m[0] for m in MUTANTS}
assert names <= {m[0] for m in MUTANTS}
assert not subprocess.check_output(['git','status','--porcelain','--','packages'],cwd=ROOT,text=True).strip(),'Commit tested packages first'
assert (ROOT/'node_modules').is_dir() and not (ROOT/'node_modules').is_symlink(),'Local install required'
before={f:sha(ROOT/f) for f in {m[1] for m in MUTANTS}}
head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()
a.output.mkdir(parents=True,exist_ok=True);results=[]
with tempfile.TemporaryDirectory(prefix='fethry266-mutants-') as temp:
temp=Path(temp);archive=temp/'source.tar'
with archive.open('wb') as stream:
subprocess.run(['git','archive','HEAD','packages','roles','contracts','tasks','missions','package.json','RELEASE'],cwd=ROOT,stdout=stream,check=True)
for name,target,old,new,test in MUTANTS:
if name not in names:continue
tree=temp/name;tree.mkdir()
with tarfile.open(archive) as bundle:bundle.extractall(tree,filter='data')
(tree/'node_modules').symlink_to(ROOT/'node_modules',target_is_directory=True)
path=tree/target;source=path.read_text();hits=source.count(old)
assert hits==1,(name,'anchor hits',hits)
path.write_text(source.replace(old,new,1));limit=240 if test==BROWSER_TEST else 120
cmd=['timeout','-k','10',str(limit),'node','--test','--test-reporter=tap','--test-concurrency=1','--test-timeout=30000',test]
signals=[];started=time.monotonic()
with (a.output/(name+'.tap')).open('w') as log:
child=subprocess.Popen(cmd,cwd=tree,env={**os.environ,'MOSAIC_REQUIRE_EDITOR_BUILD':'1'},stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:code=child.wait(timeout=limit+15)
except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL');code=child.wait()
finally:
try:
os.killpg(child.pid,signal.SIGTERM);signals.append('TERM');time.sleep(.2)
try:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL')
except ProcessLookupError:pass
except ProcessLookupError:pass
path.write_text(source)
restored=sha(path)==hashlib.sha256(source.encode()).hexdigest();unchanged=before=={f:sha(ROOT/f) for f in before}
assert restored and unchanged,(name,'restore changed')
output=(a.output/(name+'.tap')).read_text()
killed=code==1 and 'ERR_ASSERTION' in output and 'not ok ' in output and "failureType: 'hookFailed'" not in output and '# cancelled 0\n' in output and '# skipped 0\n' in output
result={'mutant':name,'test':test,'exit':code,'killed':killed,'hits':hits,'restored':restored,'candidateUnchanged':unchanged,'seconds':round(time.monotonic()-started,3),'cleanupSignals':signals}
results.append(result);print(json.dumps(result),flush=True)
if not killed:break
report={'source':head,'selected':sorted(names),'candidateUnchanged':before=={f:sha(ROOT/f) for f in before},'results':results}
(a.output/'summary.json').write_text(json.dumps(report,indent=2)+'\n')
if len(results)!=len(names) or not all(r['killed'] for r in results):raise SystemExit(1)
if __name__=='__main__':main()
@@ -0,0 +1,19 @@
packages/webui/src/public/chat.js: old=52828c03b618e3754f0e59a395f4bcc16d1c9d8db01d403406a67ca0934f0188 new=abfed92be8c57024ee590a7669cdc7cabfbfece88f92c953a3bef2bedba412dd
packages/webui/tests/chat.test.mjs:319: for (const name of ['chat.js', 'chat.css']) {
packages/webui/tests/browser.test.mjs:21: // Rows 129, 154, 191, 194 and 294: chat.js, editor.js, processing.js, agents.js and federation.js are the scripts added, same origin; the list is the pin, so another script is a visible change.
packages/webui/tests/browser.test.mjs:22: assert.deepEqual(await b.evaluate('[...document.querySelectorAll("script")].map(s => s.getAttribute("src"))'), ['/editor.js', '/processing.js', '/federation.js', '/brand.js', '/app.js', '/markdown
packages/webui/tests/browser.test.mjs:24: assert.equal(await b.evaluate(`document.querySelectorAll('script[src="/chat.js"]').length`), 1);
packages/webui/tests/chat-sessions-access.test.mjs:34: if(url.pathname === '/') {res.writeHead(200,{'content-type':'text/html'});res.end('<!doctype html><title>Session access fixture</title><div id="view"></div><script src="/chat.js"></script>');retur
packages/webui/tests/chat-sessions-access.test.mjs:35: if(url.pathname === '/chat.js') {res.writeHead(200,{'content-type':'text/javascript'});res.end(readFileSync(new URL('../src/public/chat.js',import.meta.url)));return;}
packages/webui/tests/thread-sharing-fixture.mjs:52: const source = readFileSync(new URL('../src/public/chat.js', import.meta.url), 'utf8');
packages/webui/tests/thread-sharing-fixture.mjs:69: if (url.pathname === '/chat.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(chatSource); }
packages/webui/tests/thread-sharing-fixture.mjs:72: return res.end('<!doctype html><title>Sharing fixture</title><div id="view"></div><script src="/chat.js"></script>');
literal old/current 64-hex hits in tests: []
packages/webui/src/chat-http.mjs: old=854c86ae3fa6bfcdcfedbf49c2d3b50815a3ca6ce5849ff5f1c56d1aa1a55288 new=3a5321a5f492a98d342e4ddcc1e9da3522cfff89a4f9bc3f51d2ab1426d3de1b
packages/webui/tests/chat.test.mjs:273: const { chatHTTP } = await import('../src/chat-http.mjs');
packages/webui/tests/thread-sharing-fixture.mjs:9: import { chatHTTP } from '../src/chat-http.mjs';
literal old/current 64-hex hits in tests: []
packages/plugins/channel/webui/index.mjs: old=718139cae4a0bcd10bab4b334f560c28911509e4875b04618e6880d1579d9466 new=789132601745a94027f9e9184e7fef1f70cff0cba450afed5cca647f683591f4
packages/plugins/tests/room-channel.test.mjs:3: import { dispatch } from '../channel/webui/index.mjs';
packages/cli/tests/fixtures/federation-host.mjs:51: `export * from ${JSON.stringify(new URL('../../../plugins/channel/webui/index.mjs', import.meta.url).href)};`)),
literal old/current 64-hex hits in tests: []
@@ -0,0 +1,3 @@
packages/webui/src/public/chat.js 52828c03b618e3754f0e59a395f4bcc16d1c9d8db01d403406a67ca0934f0188
packages/webui/src/chat-http.mjs 854c86ae3fa6bfcdcfedbf49c2d3b50815a3ca6ce5849ff5f1c56d1aa1a55288
packages/plugins/channel/webui/index.mjs 718139cae4a0bcd10bab4b334f560c28911509e4875b04618e6880d1579d9466
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Owned worktree gate, with a fresh process group and bounded cleanup per command."""
import json, os, signal, subprocess, sys, time
from pathlib import Path
root = Path(__file__).resolve().parents[4]
scratch = Path(sys.argv[1]); scratch.mkdir(parents=True, exist_ok=True)
env = {**os.environ, 'TMPDIR': str(scratch), 'MOSAIC_REQUIRE_EDITOR_BUILD': '1'}
env.pop('MOSAIC_LIVE_PROVIDER', None)
node = ['node', '--test', '--test-reporter=tap', '--test-concurrency=1', '--test-timeout=30000']
steps = [('editor-build', 120, ['npm', 'run', 'build:editor'], root / 'packages/webui'),
('webui-full', 900, node + ['packages/webui/tests/'], root),
('plugins-full', 300, node + ['packages/plugins/tests/'], root),
('bus-full', 1200, node + ['packages/bus/tests/'], root)]
steps += [(p.stem, 1200, ['bash', str(p)], root) for p in sorted((root / 'scripts').glob('test-*.sh'))]
if '--scripts-only' in sys.argv[2:]:
steps = [step for step in steps if step[0].startswith('test-')]
if '--t9-reruns' in sys.argv[2:]:
steps = [(f't9-rerun-{i}', 120, node + ['packages/bus/tests/library-credential.test.mjs'], root) for i in range(1, 4)] + [('bus-rerun', 1200, node + ['packages/bus/tests/'], root)]
results = []
summary = scratch / ('scripts-summary.json' if '--scripts-only' in sys.argv[2:] else 'rerun-summary.json' if '--t9-reruns' in sys.argv[2:] else 'gate-summary.json')
for name, limit, command, cwd in steps:
started = time.monotonic(); signals = []
with (scratch / (name + '.tap')).open('w') as log:
child = subprocess.Popen(['timeout', '-k', '10', str(limit), *command], cwd=cwd, env=env,
stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
try:
code = child.wait(timeout=limit + 15)
except subprocess.TimeoutExpired:
os.killpg(child.pid, signal.SIGKILL); signals.append('KILL'); code = child.wait()
finally:
# This group was created for this command; never signal any other seat's group.
try:
os.killpg(child.pid, signal.SIGTERM); signals.append('TERM'); time.sleep(.2)
try: os.killpg(child.pid, signal.SIGKILL); signals.append('KILL')
except ProcessLookupError: pass
except ProcessLookupError: pass
entry = {'step': name, 'command': command, 'cwd': str(cwd), 'outerSeconds': limit,
'exit': code, 'seconds': round(time.monotonic()-started, 3), 'cleanupSignals': signals}
results.append(entry); summary.write_text(json.dumps(results, indent=2) + '\n')
print(json.dumps(entry), flush=True)
if code: sys.exit(code if 0 < code < 126 else 1)
+166
View File
@@ -0,0 +1,166 @@
# Row 257: Q198 S1 pressure parser and pure rules evaluator
Issue #1762. Brief: `docs/plans/2026-10-21_post-v1-row-briefs.md`, "Brief: Q198 S1 pressure parser and
pure rules evaluator" and "Gate: Q198 S1" (cca206cdc). Design: `docs/plans/2026-10-21_q198-system-manager.md`
r6.1, section 8 row S1 and section 4 (D4). Owner Morgana, reviewer Filbert, gate owner Sage (sage-2196).
Post-v1: MVP rows and re-pin windows go ahead of it on the slot. Base: refactor cca206cdc.
## Recorded choices (sage-2196 accepted each)
1. **Placement.** A new package, `packages/supervisor` (`@mosaic/supervisor`), shaped like
packages/runs. Nothing on refactor parsed loadavg, PSI, meminfo or cgroup limits (`git grep`; the
only hit was an awk line in scripts/case2-precheck.sh). S2 adds the sampler process to the same
package. Packages aren't registered centrally, so no other file changes.
2. **Provider scope** (429, quota, Retry-After, the half-open probe) is out. The brief puts it in S3
and S5.
3. **PSI** is not required on a host that has never shown it, so a kernel built without PSI skips the
PSI rows instead of holding forever. Once a sample has carried PSI, it is required: a later missing
or stale PSI signal holds with `health-unknown`. That is the case sage-2196 asked for.
- Tradeoff: I latch on the first sample that carries PSI, not only on the very first sample. So PSI
that appears late and then disappears also holds. This is stricter than "present at the first
sample", and it never skips a signal the host has shown.
4. **Memory under a cgroup.** Available memory = min(MemAvailable, limit - memory.current) at each
limited level. memory.current counts page cache, so this errs toward holding.
- Follow-up, not in this row: subtract reclaimable cache using memory.stat `inactive_file`, if S2's
live samples show holds that the host's MemAvailable doesn't support.
Rulings on two readings from the build (sage-2202, re morgana-0057):
5. **PSI latch.** PSI becomes required on the first sample that carries it. This is stricter than
"present at the first sample" and fails closed: a PSI file that disappears after it was seen is a
missing signal, so health-unknown holds.
6. **PSI count across resources.** Design line 152 says "CPU some ≥20, memory full ≥2, or I/O full
≥10, for 3 samples". The window applies to the whole disjunction, so any PSI condition on 3
consecutive samples holds. If Filbert reads it per resource at review, the review finding decides,
and the fix is a single counter.
Other readings of the D4 table, made in the build:
- **Memory floors.** "Whichever demands more free memory" means the larger floor applies. Hold below
max(15% x limit, 1 GiB); release above max(25% x limit, 2 GiB).
- **Edges as written.** Hold at ≥ for load and PSI, < for memory. Release at ≤ 0.55 for load, < for
PSI, > for memory. Critical below 5% of the limit and below 1 GiB of disk.
- **Sample counts.** "For 3 samples" means 3 consecutive samples. The count resets after a gap of more
than 6 s (the stale limit) between samples. The PSI row counts as one row (ruling 6), and the
reasons name the resources over on the latest sample.
- **Memory holds on one sample.** The table's memory row has no sample count.
- **Release** needs every applicable release condition on every sample for 30 s, by the samples' own
times (`>= 30000` ms after the first qualifying sample). A dead-band sample or any hold restarts the
window, and so does a gap of more than 6 s.
- **Start held.** `initialState()` is held, because nothing has been seen yet. The first healthy
sample starts the 30 s window.
- **Missing or stale.** Required signals are load, memory, oom and disk, plus PSI once seen. A signal
holds at once with `health-unknown` if it is:
- missing;
- non-numeric, negative or zero where a divisor;
- more than 6 s older than its sample (6 s exactly is fresh);
- stamped later than its sample.
Disk and OOM are required because the critical row can't be evaluated without them.
- **Host-only OOM.** `/proc/vmstat` `oom_kill` gives the OOM counter when there's no cgroup
memory.events. The parser returns null for a kernel without it, and the sampler then has no OOM
signal (health-unknown).
- **Critical** latches. It ends only when the caller passes `reconciled` with no critical condition in
the sample, then goes through held and the normal 30 s release. The evaluator never sets
`reconciled` and has no kill-switch path. Critical reasons accumulate while latched.
- **OOM counter.** The first OOM sample is the baseline. A falling counter (a recreated cgroup) resets
the baseline with a note, not an event.
- **Clock going backwards.** A sample earlier than the last one holds with `health-unknown` (unknown
`clock`, note `clock-backwards`).
- **Swap activity** is parsed (`pswpin`, `pswpout`) and has no rule. D4 lists it as a signal with no
threshold.
- **Settings.** `resolveRules` refuses:
- unknown keys and non-numbers;
- a release at or past its hold (load, each PSI resource);
- memory release floors at or below the hold floors;
- fractions outside (0, 1);
- PSI values outside 0 to 100;
- sample counts that aren't integers ≥ 1;
- a negative release window;
- a stale limit at or below the sample interval;
- a critical memory fraction at or above the hold fraction;
- a non-positive disk floor.
- **Large byte values.** Parsed with `Number`. A memory.max above 2^53 loses precision, but it only
takes part in a min against MemTotal.
## Round 2 (Filbert r1 APPROVE c30188 notes, sage-2212)
- N1: `merge` checked unknown keys with `key in base`, so inherited names (`toString`, `constructor`,
`__proto__`) passed the check and were dropped instead of refused. It now uses
`Object.hasOwn(base, key)`, and the refusal test has those three cases.
- N2: the critical-memory edge was tested only on a 32 GiB limit, where 5% is not an integer, so a
`<` to `<=` mutant survived. The test adds a 20 GiB limit, where 5% is exactly 1 GiB, and M8 is that
mutant. I checked the other memory edges for the same gap:
- The hold floor is non-integer on 32 GiB but is tested at exactly 1 GiB on a 4 GiB limit.
- The release floors are 8 GiB on 32 GiB and 2 GiB on 4 GiB, both integers.
- The disk edge is 1 GiB.
None has the gap.
## Files
- `packages/supervisor/package.json`, `README.md`
- `src/errors.mjs`: `SupervisorError` with `kind` parse, rules or sample.
- `src/pressure.mjs`: the nine parsers, `effectiveCpus` and `effectiveMemory`.
- `src/rules.mjs`: `DEFAULT_RULES`, `resolveRules`, `initialState`, `evaluate`.
- `src/index.mjs`
- `tests/pressure.test.mjs`: parsers on kernel-shaped fixture text, with malformed, truncated and
repeated-key cases. Effective CPU cases cover the host-only fallback, cpuset, fractional and nested
`cpu.max`, and `max`. Effective memory cases cover the host-only fallback, `max`, nested levels,
usage over the limit, a limit above MemTotal and a missing memory.current.
- `tests/rules.test.mjs`: the D4 table on a fake clock.
- Each hold and release threshold on both sides of its edge.
- Three-sample counting and the gap reset.
- The 30 s release at 29 999 and 30 000 ms, and the dead-band restart.
- Missing, stale, future and malformed signals.
- PSI never present, and PSI lost after it was present.
- Critical OOM, memory and disk at their edges, the reconciliation latch and the counter reset.
- A backwards clock, input state left unchanged, and settings refusals and overrides.
- `tests/purity.test.mjs`: a static check that src imports only its own files and uses no
fs, child_process, net, os, process, dynamic import, Date or timers.
No test reads /proc, /sys or a cgroup, starts a build, opens a network socket or generates load. No new
dependency.
## Mutants (each must fail its named test; the brief requires M1 and M2)
| Mutant | Edit | Named test |
|---|---|---|
| M1 hold edge | `ratio >= rules.load.hold` becomes `>` | load holds at 0.80 for 3 samples and not at 0.7999 |
| M2 early release | `now - next.releaseSince >= rules.releaseMs` becomes `>= 0` | the first state is held, and healthy samples open it after exactly 30 s |
| M3 memory floor | the hold floor `Math.max` becomes `Math.min` | memory holds below the larger floor, on one sample |
| M4 sample count | `next.loadCount >= rules.load.samples` becomes `>= 1` | load holds at 0.80 for 3 samples and not at 0.7999 |
| M5 stale | the `now - signal.at > rules.staleMs` check is removed | a signal is stale at more than 6 s old and fresh at exactly 6 s |
| M6 effective CPU | the quota `Math.min` becomes `Math.max` | effective CPUs take the smallest of host, cpuset and every quota level |
| M7 critical latch | the `!reconciled` critical branch is removed | critical stays until reconciliation, then still waits the 30 s release |
| M8 critical memory edge (r2) | `available < rules.critical.memoryFraction * limit` becomes `<=` | memory under 5% and disk under 1 GiB are critical, at their edges |
## Gate (Sage's slot, on CLEAR only)
- The three test files, each whole, then `node --test tests/` in packages/supervisor as the package
suite.
- Then M1 to M8, each on its named test, restored after each.
- umask 022, TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`.
- The kit is /mnt/storage/scratch/morgana-r257 (slot257.sh, mutate.py). It refuses unless HEAD is the
candidate and the tree is clean, and stops after two unexpected failures (mos-0823).
- Static checks before the slot: `node --check` on every file, run from `/`. No test has run yet.
## Slot result (sage-4159 CLEAR, 2026-10-24 17:00:27Z to 17:00:30Z, candidate 1261fa2d8)
Run as `busy.sh && timeout -k 10 1500 slot257.sh` in its own session (setsid). The busy check read
CLEAR. The worktree was checked out at 1261fa2d8 with a clean tree. Filbert's approved code is
unchanged. The summary is in `gate-run.log` and the per-run node output in `mutants-run.log`.
| Run | tests | pass | fail | skip | cancelled | todo |
|---|---|---|---|---|---|---|
| tests/pressure.test.mjs whole | 15 | 15 | 0 | 0 | 0 | 0 |
| tests/rules.test.mjs whole | 29 | 29 | 0 | 0 | 0 | 0 |
| tests/purity.test.mjs whole | 1 | 1 | 0 | 0 | 0 | 0 |
| package suite `node --test tests/` | 45 | 45 | 0 | 0 | 0 | 0 |
Mutants: 8 of 8 killed. Each one ran only its named test (1 test, rc=1), and each failure was an
AssertionError in that test, not a crash: M1 hold edge, M2 early release, M3 memory floor, M4 sample
count, M5 stale, M6 effective CPU, M7 critical latch, M8 critical memory edge.
Restore check: `git checkout -- packages/supervisor` ran after each mutant, and the script found the
tree clean every time. At the end HEAD was 1261fa2d8 and `git status --porcelain` was empty. After the
run no node, python or bash process had its cwd under the kit or the worktree, and no g257 TMPDIR was
left behind. The run reached no scripts/test-*.sh.
@@ -0,0 +1,34 @@
CLEAR
== files 2026-10-24T17:00:27Z
file-pressure rc=0 1s ℹ pass 15 ℹ fail 0
file-rules rc=0 0s ℹ pass 29 ℹ fail 0
file-purity rc=0 0s ℹ pass 1 ℹ fail 0
== package suite 2026-10-24T17:00:28Z
suite-supervisor rc=0 0s ℹ pass 45 ℹ fail 0
== mx 2026-10-24T17:00:28Z (each must fail its named test)
M1 applied to rules.mjs
mx-M1 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M1 KILLED
M2 applied to rules.mjs
mx-M2 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M2 KILLED
M3 applied to rules.mjs
mx-M3 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M3 KILLED
M4 applied to rules.mjs
mx-M4 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M4 KILLED
M5 applied to rules.mjs
mx-M5 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M5 KILLED
M6 applied to pressure.mjs
mx-M6 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M6 KILLED
M7 applied to rules.mjs
mx-M7 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M7 KILLED
M8 applied to rules.mjs
mx-M8 rc=1 0s ℹ pass 0 ℹ fail 1
mx-M8 KILLED
restored at 1261fa2d8
SLOT257_DONE 2026-10-24T17:00:30Z
@@ -0,0 +1,391 @@
===== file-pressure.log
✔ loadavg parses the three loads (1.192277ms)
✔ a malformed loadavg throws (0.412303ms)
✔ meminfo returns bytes and keeps unitless counts (0.258214ms)
✔ meminfo without MemAvailable, with a repeated key or a bad line throws (0.206592ms)
✔ PSI parses some and full, and cpu may lack full (0.374747ms)
✔ PSI without a full line for memory or io, or with a bad line, throws (0.195229ms)
✔ CPU lists count distinct CPUs (0.237299ms)
✔ cpu.max gives the quota in CPUs, or null for max (0.23685ms)
✔ memory.max, memory.current and memory.events (0.351243ms)
✔ vmstat gives swap counters and the host OOM counter (0.234271ms)
✔ effective CPUs fall back to the host count with no cgroup input (0.119728ms)
✔ effective CPUs take the smallest of host, cpuset and every quota level (0.117052ms)
✔ effective memory falls back to the host with no cgroup levels, or only max (0.609639ms)
✔ effective memory uses the tightest limited level (0.187466ms)
✔ a limited level without memory.current throws (0.096904ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 171.66419
===== file-rules.log
✔ the first state is held, and healthy samples open it after exactly 30 s (2.048214ms)
✔ load holds at 0.80 for 3 samples and not at 0.7999 (1.121376ms)
✔ load divides by effective CPUs, fractional ones included (0.360874ms)
✔ a gap of more than 6 s resets the consecutive count (0.343221ms)
✔ load releases at 0.55 and not at 0.5501 (0.435882ms)
✔ a dead-band sample restarts the 30 s release window (0.376884ms)
✔ memory holds below the larger floor, on one sample (0.259125ms)
✔ on a small limit the 1 GiB floor is the larger one (0.346965ms)
✔ memory releases only above the larger release floor (0.906516ms)
✔ each PSI hold threshold holds at its value for 3 samples and not just below (0.661209ms)
✔ the PSI count runs across resources (0.15624ms)
✔ each PSI release threshold blocks at its value and releases just below (0.613751ms)
✔ a missing required signal holds at once with health-unknown (0.388768ms)
✔ a signal is stale at more than 6 s old and fresh at exactly 6 s (0.446828ms)
✔ malformed signal values count as missing (0.787389ms)
✔ after health-unknown, recovery waits the full 30 s (0.34472ms)
✔ a host that never had PSI skips the PSI rows (0.201995ms)
✔ PSI that disappears after it was present holds with health-unknown (0.634442ms)
✔ a rise in the OOM kill counter is critical; the first sample is the baseline (0.230518ms)
✔ a falling OOM counter resets the baseline with a note, not an event (0.208182ms)
✔ memory under 5% and disk under 1 GiB are critical, at their edges (0.800954ms)
✔ critical stays until reconciliation, then still waits the 30 s release (0.585483ms)
✔ critical reasons accumulate while critical (0.174367ms)
✔ a clock that goes backwards holds with health-unknown (0.165886ms)
✔ a sample without a numeric time throws (0.515819ms)
✔ evaluate doesn't change the state it was given (0.327166ms)
✔ resolveRules with no overrides is the D4 table, frozen (0.502534ms)
✔ overrides apply in the evaluator (0.291524ms)
✔ contradictory or unknown settings refuse (1.168994ms)
ℹ tests 29
ℹ suites 0
ℹ pass 29
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 86.686704
===== file-purity.log
✔ src reads no file, process, network or clock (1.611513ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 63.38547
===== suite-supervisor.log
✔ loadavg parses the three loads (1.322312ms)
✔ a malformed loadavg throws (0.491714ms)
✔ meminfo returns bytes and keeps unitless counts (0.306568ms)
✔ meminfo without MemAvailable, with a repeated key or a bad line throws (0.282108ms)
✔ PSI parses some and full, and cpu may lack full (0.382364ms)
✔ PSI without a full line for memory or io, or with a bad line, throws (0.229427ms)
✔ CPU lists count distinct CPUs (0.310693ms)
✔ cpu.max gives the quota in CPUs, or null for max (0.258658ms)
✔ memory.max, memory.current and memory.events (0.36499ms)
✔ vmstat gives swap counters and the host OOM counter (0.27834ms)
✔ effective CPUs fall back to the host count with no cgroup input (0.148614ms)
✔ effective CPUs take the smallest of host, cpuset and every quota level (0.118641ms)
✔ effective memory falls back to the host with no cgroup levels, or only max (0.189321ms)
✔ effective memory uses the tightest limited level (0.248126ms)
✔ a limited level without memory.current throws (0.174502ms)
✔ src reads no file, process, network or clock (1.698537ms)
✔ the first state is held, and healthy samples open it after exactly 30 s (2.722678ms)
✔ load holds at 0.80 for 3 samples and not at 0.7999 (1.31022ms)
✔ load divides by effective CPUs, fractional ones included (0.457608ms)
✔ a gap of more than 6 s resets the consecutive count (0.487291ms)
✔ load releases at 0.55 and not at 0.5501 (0.471881ms)
✔ a dead-band sample restarts the 30 s release window (0.407363ms)
✔ memory holds below the larger floor, on one sample (0.311888ms)
✔ on a small limit the 1 GiB floor is the larger one (0.337859ms)
✔ memory releases only above the larger release floor (0.852137ms)
✔ each PSI hold threshold holds at its value for 3 samples and not just below (0.670722ms)
✔ the PSI count runs across resources (0.168238ms)
✔ each PSI release threshold blocks at its value and releases just below (0.706245ms)
✔ a missing required signal holds at once with health-unknown (0.45638ms)
✔ a signal is stale at more than 6 s old and fresh at exactly 6 s (0.501209ms)
✔ malformed signal values count as missing (0.703616ms)
✔ after health-unknown, recovery waits the full 30 s (0.252741ms)
✔ a host that never had PSI skips the PSI rows (0.136341ms)
✔ PSI that disappears after it was present holds with health-unknown (0.302628ms)
✔ a rise in the OOM kill counter is critical; the first sample is the baseline (0.181417ms)
✔ a falling OOM counter resets the baseline with a note, not an event (0.166744ms)
✔ memory under 5% and disk under 1 GiB are critical, at their edges (0.59429ms)
✔ critical stays until reconciliation, then still waits the 30 s release (0.346404ms)
✔ critical reasons accumulate while critical (0.111712ms)
✔ a clock that goes backwards holds with health-unknown (0.098686ms)
✔ a sample without a numeric time throws (0.425754ms)
✔ evaluate doesn't change the state it was given (0.239173ms)
✔ resolveRules with no overrides is the D4 table, frozen (0.368226ms)
✔ overrides apply in the evaluator (0.251687ms)
✔ contradictory or unknown settings refuse (0.917827ms)
ℹ tests 45
ℹ suites 0
ℹ pass 45
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 92.173971
===== mx-M1.log
✖ load holds at 0.80 for 3 samples and not at 0.7999 (2.627622ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 53.826624
✖ failing tests:
test at tests/rules.test.mjs:75:1
✖ load holds at 0.80 for 3 samples and not at 0.7999 (2.627622ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
'open',
'open',
+ 'open'
- 'held'
]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:77:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ 'open', 'open', 'open' ],
expected: [ 'open', 'open', 'held' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
===== mx-M2.log
✖ the first state is held, and healthy samples open it after exactly 30 s (1.670671ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 48.463372
✖ failing tests:
test at tests/rules.test.mjs:62:1
✖ the first state is held, and healthy samples open it after exactly 30 s (1.670671ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
'open' !== 'held'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:65:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'open',
expected: 'held',
operator: 'strictEqual',
diff: 'simple'
}
===== mx-M3.log
✖ memory holds below the larger floor, on one sample (2.628385ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.087261
✖ failing tests:
test at tests/rules.test.mjs:119:1
✖ memory holds below the larger floor, on one sample (2.628385ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
'open' !== 'held'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:125:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'open',
expected: 'held',
operator: 'strictEqual',
diff: 'simple'
}
===== mx-M4.log
✖ load holds at 0.80 for 3 samples and not at 0.7999 (2.72525ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.494912
✖ failing tests:
test at tests/rules.test.mjs:75:1
✖ load holds at 0.80 for 3 samples and not at 0.7999 (2.72525ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
- 'open',
- 'open',
'held',
+ 'held',
+ 'held'
]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:77:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ 'held', 'held', 'held' ],
expected: [ 'open', 'open', 'held' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
===== mx-M5.log
✖ a signal is stale at more than 6 s old and fresh at exactly 6 s (2.946775ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 58.672563
✖ failing tests:
test at tests/rules.test.mjs:187:1
✖ a signal is stale at more than 6 s old and fresh at exactly 6 s (2.946775ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
+ []
- [
- 'load'
- ]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:191:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [],
expected: [ 'load' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
===== mx-M6.log
✖ effective CPUs take the smallest of host, cpuset and every quota level (1.625079ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 55.012035
✖ failing tests:
test at tests/pressure.test.mjs:101:1
✖ effective CPUs take the smallest of host, cpuset and every quota level (1.625079ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 1.5
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/pressure.test.mjs:103:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1.5,
operator: 'strictEqual',
diff: 'simple'
}
===== mx-M7.log
✖ critical stays until reconciliation, then still waits the 30 s release (4.842568ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.317924
✖ failing tests:
test at tests/rules.test.mjs:259:1
✖ critical stays until reconciliation, then still waits the 30 s release (4.842568ms)
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert.ok(waiting.decisions.every((d) => d.mode === "critical" && d.reasons[0] === "critical-disk"))
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:262:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
===== mx-M8.log
✖ memory under 5% and disk under 1 GiB are critical, at their edges (3.080014ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 49.440823
✖ failing tests:
test at tests/rules.test.mjs:245:1
✖ memory under 5% and disk under 1 GiB are critical, at their edges (3.080014ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
'critical' !== 'held'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/morgana-row257/packages/supervisor/tests/rules.test.mjs:253:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'critical',
expected: 'held',
operator: 'strictEqual',
diff: 'simple'
}
+3
View File
@@ -1678,3 +1678,6 @@ are never rewritten or removed; corrections are new entries.
- 2026-10-24 — steelbeak — row 287 C2-21 (#1792), builder complete under sage-4130: collaborator library grants and chief real read mint built; bus 998/business 283/CLI 989+FED1/Docker2/install27/all12 shell gates green, M1-M11 killed/restored, token/cleanup audits clean; preparing steelbeak/row287 push and manifest handoff to Boyd then Dewey, no live change or landing.
- 2026-10-24 — steelbeak — row 287 first-review follow-up: Boyd approved c2338ed58/3683b40... with two doc-only corrections; append-only BUILD correction distinguishes clean product/test diff from raw-log whitespace and records old-test numstats. Queue round1 open; missing-seat-credential request failure reported to Sage. Frozen product and manifest preserved; no tests or slot use.
2026-10-24T16:12:35Z | Sage (1ef1e4f8) | Rows 225 and 287 landed (mos-1576, mos-1590) | 225 9b6345466 (Dewey c31599, gate GREEN 15:54:58Z, #1726 closed c31604); 287 0e935776e (Boyd c31602, Dewey c31603, gate GREEN 16:11:38Z, #1792 closed c31606); F-C owner-push follow-up and Dewey N1-N3 recorded in BUILD-LOG; no re-pin.
2026-10-24 | Fethry14f80e3c | Row266 C2-10c (#1771), sage4154/4156 | New fethry/row266 worktree from962046aaa, doc-only fast-forward487500a16. Read pinned brief and amendment; local npm ci roots, no links. Identified room.read missing guard check/last; Sage authorized bounded broker hydration amendment and re-pin. Owner start retried after stale-pin refusal; design/current-code notes in agents/fethry/work/queue-266/BUILD.md. Slot held; RED-first new tests next; no production edits yet.
2026-10-24 | Fethry14f80e3c | Row266 C2-10c (#1771) implementation complete for review | Source3095d6ea5; manifest5f81604210 paths. Full webui458/0, plugins120/0, bus1000/0,12 scripts0,16/16 mutants. Initial old T9 timer race and sage4158 unchanged6/0 x3 plus1000/0 rerun preserved. No old tests changed; own-cwd process sweep empty. Slot returned with push handoff; Boyd then Dewey, Sage landing pending.
+50 -7
View File
@@ -101,7 +101,16 @@ export const TASK_PAGE_MAX = 200;
// request allowed past the ordinary 64 KiB envelope (server.mjs and client.mjs), up to its content cap.
export const PROJECT_FILE_VERBS = Object.freeze(['project.files', 'project.file', 'project.file.save']);
// C2-03a: library.sync is the human's fetch, and with `accept` the one way past library-diverged.
export const LIBRARY_VERBS = Object.freeze(['library.file', 'library.status', 'library.sync']);
// Row 224: pure agent lookups are separate from the three human-only verbs. Listing them
// here also makes server.mjs await their host effects instead of serializing a Promise as {}.
const LIBRARY_LOOKUPS = Object.freeze({
'library.find': { method: 'find', fields: ['universe', 'query', 'type', 'book', 'scope', 'offset'], required: ['universe', 'query'] },
'library.get': { method: 'get', fields: ['universe', 'path', 'from', 'links_from'], required: ['universe', 'path'] },
'library.search': { method: 'search', fields: ['universe', 'text', 'book', 'scope', 'offset'], required: ['universe', 'text'] },
'library.manuscript': { method: 'manuscript', fields: ['universe', 'book', 'scope', 'chapter', 'from', 'to'], required: ['universe', 'book'] },
'library.universe': { method: 'universeInfo', fields: ['universe'], required: ['universe'] },
});
export const LIBRARY_VERBS = Object.freeze(['library.file', 'library.status', 'library.sync', ...Object.keys(LIBRARY_LOOKUPS)]);
export const PROJECT_SAVE_VERB = 'project.file.save';
export const PROJECT_SAVE_REQUEST_MAX = 1024 * 1024 + 64 * 1024;
export const WEB_VERBS =Object.freeze(['web.search', 'web.fetch']);
@@ -4851,7 +4860,7 @@ export class Broker {
if (this.#guardPass?.s !== s) {
if (request.verb === 'message.send' && this.#guarded(s, args.to))
return this.#guardedSend(cap, s, request, structuredClone(args));
if (request.verb === 'thread.messages' && this.#libraryProject(s.business) && this.#library?.last)
if (['thread.messages', 'room.read'].includes(request.verb) && this.#libraryProject(s.business) && this.#library?.last)
return this.#withLast(cap, s, request);
}
if (CREDENTIAL_VERBS.includes(request.verb)) return this.#requestCredential(s, request.verb, structuredClone(args));
@@ -5352,10 +5361,42 @@ export class Broker {
throw this.#refused(s, ProjectFailure && e instanceof ProjectFailure ? new BusError(e.code) : e, request);
}
}
// Row 224 C2-05b: the agent never supplies a host repo, commit or business. Its current
// holder and business entry are rechecked before/after awaits; runtime owns Git and queries.
async #requestLibraryLookup(s, { verb, args }) {
try {
this.#agent(s);
const business = this.#business(s.business);
const projects = Object.entries(business.projects ?? {}).filter(([, entry]) => entry.kind === 'library');
if (!projects.length || typeof this.#library?.lookup !== 'function') fail('library-not-configured');
const spec = LIBRARY_LOOKUPS[verb];
keys(args, ['project', ...spec.fields], spec.required);
if (!text(args.universe, 160)) fail('invalid-request');
if (args.project === undefined && projects.length !== 1) fail('invalid-request');
const project = args.project ?? projects[0][0];
if (!text(project, 160) || !projects.some(([name]) => name === project)) fail('library-not-configured');
const revision = JSON.stringify(business.projects[project]);
const authorize = () => {
this.#agent(s);
this.#processingGate(s, verb);
if (JSON.stringify(this.#business(s.business).projects?.[project]) !== revision) fail('library-not-configured');
};
const { project: ignored, ...query } = args;
authorize();
const result = await this.#library.lookup({ business, project }, { method: spec.method, args: query, authorize });
authorize();
this.#secretCheck(result);
return result;
} catch (error) {
const Failure = this.#library?.LibraryError;
throw this.#refused(s, Failure && error instanceof Failure ? new BusError(error.code) : error, { verb, args });
}
}
// C2-04 M4: only a verified human reads library text or status. Host Git
// functions stay injected, preserving this module's sandbox import closure.
async #requestLibrary(s, request) {
const { verb, args } = request;
if (Object.hasOwn(LIBRARY_LOOKUPS, verb)) return this.#requestLibraryLookup(s, request);
try {
this.#human(s);
const fields = verb === 'library.file' ? ['project', 'path'] : verb === 'library.sync' ? ['project', 'accept'] : ['project'];
@@ -5424,6 +5465,11 @@ export class Broker {
}
return this.#reenter(cap, s, { verb: 'message.send', args }, check);
}
// The same immutable check evidence is attached to both thread and merged room reads.
#withCheck(m) {
const e = this.#store.get("SELECT body FROM events WHERE id=? AND kind='action.allowed' AND subject=?", checkEventId(m.id), m.id);
return e ? { ...m, check: JSON.parse(e.body).check } : m;
}
// C2-06 (sage-3595, morgana-0200): each path cite of a checked row gains `last`, the file's last commit
// at the served tip, one git call per distinct path. Rows without `check` are unchanged.
async #withLast(cap, s, request) {
@@ -6700,7 +6746,7 @@ export class Broker {
}
previous = m;
}
return entries;
return entries.map((m) => this.#withCheck(m));
}
// A thread belongs to the user who made it and to one declared thread instance.
// Row 177 (#1675): a tester's thread is owned by `tester`, so neither reaches the other's.
@@ -6827,10 +6873,7 @@ export class Broker {
`SELECT m.* FROM messages m JOIN message_threads mt ON mt.message=m.id WHERE mt.session=? AND m.seq>? ORDER BY m.seq LIMIT ${limit}`,
session,
after,
).map((m) => {
const e = this.#store.get("SELECT body FROM events WHERE id=? AND kind='action.allowed' AND subject=?", checkEventId(m.id), m.id);
return e ? { ...m, check: JSON.parse(e.body).check } : m;
});
).map((m) => this.#withCheck(m));
}
// Row 206 (193b2, #1704, design 5.3): the owner of a thread shares it read-only, or ends a share.
// A user's collaborator grant must still cover the thread, and a user target needs a live grant
+34
View File
@@ -87,6 +87,38 @@ export async function harnessLevels({ log = (e) => process.stderr.write(`plugins
}
return (name) => levels.get(name) ?? null;
}
// Row 224 C2-05b: a host-only adapter. A failed fetch keeps serving immutable old
// objects, as C2-03a requires; no caller may select a repo, commit or sync --accept.
export function createLibraryLookup({ dataRoot, sync, now = () => Date.now() }) {
const methods = { find: libraryModule.find, get: libraryModule.get, search: libraryModule.search,
manuscript: libraryModule.manuscript, universeInfo: libraryModule.universeInfo };
return async (context, { method, args, authorize }) => {
if (!Object.hasOwn(methods, method)) throw new libraryModule.LibraryError('query-argument-invalid', 'unknown lookup');
authorize();
const source = { ...context, dataRoot };
let state = await libraryModule.libraryStatus(source);
authorize();
// sage-2819 / dewey-0852: failed attempts also pace inline retries. Otherwise an
// outage makes every lookup refetch while lastFetch remains old or null.
const recentAttempt = Math.max(state.lastFetch === null ? -Infinity : Date.parse(state.lastFetch),
state.lastError === null ? -Infinity : Date.parse(state.lastError.at));
const age = now() - recentAttempt;
if (age > libraryModule.SYNC_INTERVAL_MS || age < 0) {
try { await sync(source); }
catch (error) {
// A timer may already be fetching this mirror. Its current served state is still safe.
if (!(error instanceof libraryModule.LibraryError) || error.code !== 'library-sync-busy') throw error;
}
authorize();
state = await libraryModule.libraryStatus(source);
}
if (!state.served) throw new libraryModule.LibraryError('library-commit', 'library has never been synced');
authorize();
return methods[method]({ repo: libraryModule.mirrorPath(source), commit: state.served,
mapping: libraryModule.checkMapping(context.business.projects[context.project]) }, args);
};
}
export async function startBroker({
dataRoot,
businesses,
@@ -310,6 +342,8 @@ export async function startBroker({
store,
businesses,
library: {
lookup: createLibraryLookup({ dataRoot, sync: context =>
processing.work(() => libraryModule.sync(context, syncOptions(context.business))) }),
readFile: context => libraryModule.readLibraryFile({ ...context, dataRoot }),
status: context => libraryModule.libraryStatus({ ...context, dataRoot }),
sync: (context, { accept }) => processing.work(() => libraryModule.sync({ ...context, dataRoot }, { ...syncOptions(context.business), accept })),
+237
View File
@@ -0,0 +1,237 @@
// Row 224: exercise broker authorization, real C2-05a queries and host served snapshots.
// All bodies and remotes are made up. External sync is replaced only at its trusted host seam.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
import { createLibraryLookup, startBroker } from '../src/runtime.mjs';
import { serve } from '../src/server.mjs';
import { Client } from '../src/client.mjs';
import * as library from '../../library/src/index.mjs';
import { scratchRepo, note } from '../../library/tests/query-fixture.mjs';
import { mappingEntry, git } from '../../library/tests/helpers.mjs';
const NOW = Date.parse('2026-10-22T16:00:00.000Z');
const VERBS = ['library.find', 'library.get', 'library.search', 'library.manuscript', 'library.universe'];
const GET = { universe: 'harbor', path: 'Book One/Characters/Tomas.md' };
const ARGS = [
{ universe: 'harbor', query: 'Tomas' }, GET,
{ universe: 'harbor', text: 'ferry' }, { universe: 'harbor', book: 'Book One' }, { universe: 'harbor' },
];
const TEXTS = {
'Book One/Characters/Tomas.md': note('character', 'Tomas', 'He runs the ferry. [[Elsewhere]]'),
'Book One/Chapters/01.md': '# A ferry\nOne line.\n',
'Book One/Playground/What If.md': note('character', 'Maybe Tomas', 'Unwritten ferry.'),
'Second World/Characters/Elsewhere.md': note('character', 'Elsewhere', 'Other ferry.'),
};
async function world(t, { projects, adapter, claimed = true, now = () => NOW } = {}) {
const f = scratchRepo(t, TEXTS), dataRoot = join(f.root, 'data');
mkdirSync(dataRoot);
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
projects: projects ?? { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
const context = { business, project: 'books', dataRoot };
let mirror = null, state = null, fetches = 0;
if (business.projects.books?.kind === 'library') {
mirror = await library.ensureMirror(context);
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
state = { version: 1, branch: 'main', served: f.commit, lastFetch: new Date(NOW).toISOString(),
lastError: null, diverged: null, problems: [] };
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify(state), { mode: 0o600 });
}
const setState = patch => {
state = { ...state, ...patch };
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify(state), { mode: 0o600 });
};
const lookup = createLibraryLookup({ dataRoot, now, sync: async () => {
fetches++;
setState({ lastFetch: new Date(now()).toISOString() });
return { state, code: null };
} });
const store = new Store(dataRoot);
t.after(() => store.close());
const broker = new Broker({ store, businesses: { acme: business },
library: adapter ?? { lookup, LibraryError: library.LibraryError } });
const agent = broker.bindLaunch({ business: 'acme', role: 'coder', run: 'r224', harness: 'pi' });
if (claimed) broker.request(agent, { verb: 'role.claim' });
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
const reader = broker.bindReader({ business: 'acme' }), tester = broker.bindTester({ business: 'acme' });
const call = (verb, args = {}, cap = agent) => Promise.resolve().then(() => broker.request(cap, { verb, args }));
return { ...f, dataRoot, business, context, mirror, store, broker, agent, human, reader, tester,
call, lookup, setState, fetches: () => fetches };
}
test('all five agent verbs read the configured served commit through real queries', async t => {
const f = await world(t);
const [find, get, search, manuscript, universe] = await Promise.all(VERBS.map((verb, i) => f.call(verb, ARGS[i])));
for (const result of [find, get, search, manuscript, universe]) assert.equal(result.commit, f.commit);
assert.deepEqual(find.results.map(x => x.path), ['Book One/Characters/Tomas.md']);
assert.match(JSON.stringify(get.lines), /He runs the ferry/);
assert.deepEqual(get.links_out, []); // The only target is in another universe.
assert.ok(search.results.length > 0);
assert.ok(search.results.every(x => !x.path.startsWith('Second World/') && x.scope === 'canon'));
assert.equal(manuscript.path, 'Book One/Chapters/01.md');
assert.deepEqual(universe.books, ['Book One']);
assert.equal(f.fetches(), 0);
const playground = await f.call('library.find', { universe: 'harbor', query: '', scope: 'playground' });
assert.deepEqual(playground.results.map(x => x.path), ['Book One/Playground/What If.md']);
assert.equal(await f.call('library.get', { universe: 'harbor', path: 'Second World/Characters/Elsewhere.md' }), null);
});
test('each verb refuses missing universe and unknown keys before host work', async t => {
let calls = 0;
const f = await world(t, { adapter: { lookup: () => { calls++; } } });
for (const [i, verb] of VERBS.entries()) {
const { universe, ...missing } = ARGS[i];
await assert.rejects(f.call(verb, missing), { code: 'invalid-request' });
for (const extra of [{ repo: '/elsewhere' }, { commit: 'b'.repeat(40) }, { business: 'other' }, { accept: 'b'.repeat(40) }])
await assert.rejects(f.call(verb, { ...ARGS[i], ...extra }), { code: 'invalid-request' });
await assert.rejects(f.call(verb, { ...ARGS[i], universe: '' }), { code: 'invalid-request' });
}
assert.equal(calls, 0);
});
test('no library, missing host adapter and undeclared project refuse library-not-configured', async t => {
const noLibrary = await world(t, { projects: {} });
const noAdapter = await world(t, { adapter: {} });
const f = await world(t);
for (const [i, verb] of VERBS.entries()) {
await assert.rejects(noLibrary.call(verb, ARGS[i]), { code: 'library-not-configured' });
await assert.rejects(noAdapter.call(verb, ARGS[i]), { code: 'library-not-configured' });
await assert.rejects(f.call(verb, { ...ARGS[i], project: 'other' }), { code: 'library-not-configured' });
}
f.business.projects.second = mappingEntry();
f.broker.reloadBusiness('acme', f.business);
await assert.rejects(f.call('library.get', GET), { code: 'invalid-request' });
assert.equal((await f.call('library.get', { ...GET, project: 'books' })).commit, f.commit);
});
test('reads need the current holder on each call; human/tester/reader are not agents', async t => {
const f = await world(t, { claimed: false });
for (const [i, verb] of VERBS.entries()) {
await assert.rejects(f.call(verb, ARGS[i]), { code: 'not-holder' });
await assert.rejects(f.call(verb, ARGS[i], f.human), { code: 'agent-required' });
await assert.rejects(f.call(verb, ARGS[i], f.tester), { code: 'tester-refused' });
await assert.rejects(f.call(verb, ARGS[i], f.reader), { code: 'read-only' });
}
f.broker.request(f.agent, { verb: 'role.claim' });
assert.equal((await f.call('library.get', GET)).commit, f.commit);
f.broker.request(f.agent, { verb: 'role.release' });
await assert.rejects(f.call('library.get', GET), { code: 'not-holder' });
});
test('releasing a claim during an async lookup fences the response', { timeout: 30_000 }, async t => {
let release, entered;
const started = new Promise(resolve => { entered = resolve; });
const f = await world(t, { adapter: { lookup: async () => {
entered(); await new Promise(resolve => { release = resolve; }); return { lines: [[1, 'not returned']] };
} } });
const pending = f.call('library.get', GET);
await started;
f.broker.request(f.agent, { verb: 'role.release' });
release();
await assert.rejects(pending, { code: 'not-holder' });
});
test('a business library removed during an async read cannot leak its result', { timeout: 30_000 }, async t => {
let release, entered;
const started = new Promise(resolve => { entered = resolve; });
const f = await world(t, { adapter: { lookup: async () => {
entered(); await new Promise(resolve => { release = resolve; }); return { lines: [[1, 'not returned']] };
} } });
const pending = f.call('library.get', GET);
await started;
delete f.business.projects.books;
f.broker.reloadBusiness('acme', f.business);
release();
await assert.rejects(pending, { code: 'library-not-configured' });
});
test('freshness boundary, unknown last fetch and failed fetch still use immutable served state', async t => {
const f = await world(t);
f.setState({ lastFetch: new Date(NOW - 60_000).toISOString() });
await f.call('library.get', GET);
assert.equal(f.fetches(), 0);
f.setState({ lastFetch: new Date(NOW - 60_001).toISOString() });
await f.call('library.get', GET);
assert.equal(f.fetches(), 1);
f.setState({ lastFetch: null });
await f.call('library.universe', { universe: 'harbor' });
assert.equal(f.fetches(), 2);
let attempted = 0, clock = NOW;
const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => clock, sync: async () => {
attempted++;
f.setState({ lastError: { code: 'library-fetch-failed', at: new Date(clock).toISOString() } });
return { code: 'library-fetch-failed' };
} });
f.setState({ lastFetch: null });
const result = await failing(f.context, { method: 'get', args: GET, authorize() {} });
assert.equal(attempted, 1);
assert.equal(result.commit, f.commit);
for (const elapsed of [1, 59_999, 60_000]) {
clock = NOW + elapsed;
const retry = await failing(f.context, { method: 'get', args: GET, authorize() {} });
assert.equal(retry.commit, f.commit, 'serve the old pin during the failure cooldown');
assert.equal(attempted, 1, 'recent lastError prevents another sync even though lastFetch is null');
}
clock = NOW + 60_001;
assert.equal((await failing(f.context, { method: 'get', args: GET, authorize() {} })).commit, f.commit);
assert.equal(attempted, 2, 'retry only after the failed-attempt interval expires');
});
test('an unreadable sync state fails closed; a busy sync keeps serving the existing pin', async t => {
const f = await world(t);
const lookup = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => {
throw new library.LibraryError('library-sync-busy', 'another fetch');
} });
f.setState({ lastFetch: null });
assert.equal((await lookup(f.context, { method: 'get', args: GET, authorize() {} })).commit, f.commit);
writeFileSync(join(f.mirror, library.SYNC_STATE_FILE), '{bad json', { mode: 0o600 });
await assert.rejects(f.call('library.get', GET), { code: 'library-state-invalid' });
});
test('a stale lookup sees the commit published by its sync, not the prior served commit', async t => {
const f = await world(t);
writeFileSync(join(f.repo, GET.path), note('character', 'Tomas', 'Changed ferry.'));
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'second fixture');
const next = git(f.repo, 'rev-parse', 'HEAD');
const lookup = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => {
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', f.mirror, 'HEAD:refs/heads/served');
f.setState({ served: next, lastFetch: new Date(NOW).toISOString() });
return { code: null };
} });
f.setState({ lastFetch: null });
const result = await lookup(f.context, { method: 'get', args: GET, authorize() {} });
assert.equal(result.commit, next);
assert.match(JSON.stringify(result.lines), /Changed ferry/);
});
test('real socket awaits lookup results and rechecks rejected query arguments', { timeout: 30_000 }, async t => {
const f = await world(t);
const path = join(f.root, 'lookup.sock');
const server = await serve({ broker: f.broker, path }); t.after(() => server.close());
const client = new Client({ path, cap: f.agent });
const result = await client.call('library.get', GET);
assert.equal(result.commit, f.commit);
await assert.rejects(client.call('library.find', { universe: 'harbor', query: '', scope: 'anything' }), /query-argument-invalid/);
});
test('runtime actually injects the lookup adapter, without exposing human library.file', { timeout: 30_000 }, async t => {
const f = scratchRepo(t, TEXTS), dataRoot = join(f.root, 'runtime'); mkdirSync(dataRoot);
const business = { id: 'acme', human: 'jason', plugins: { harness: ['pi'] }, projects: { books: mappingEntry() },
arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
const runtime = await startBroker({ dataRoot, businesses: { acme: business } }); t.after(() => runtime.close());
const mirror = library.mirrorPath({ business, project: 'books', dataRoot });
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify({ version: 1, branch: 'main', served: f.commit,
lastFetch: new Date().toISOString(), lastError: null, diverged: null, problems: [] }), { mode: 0o600 });
const cap = runtime.broker.bindLaunch({ business: 'acme', role: 'coder', run: 'r224-runtime', harness: 'pi' });
runtime.broker.request(cap, { verb: 'role.claim' });
assert.equal((await runtime.broker.request(cap, { verb: 'library.get', args: GET })).commit, f.commit);
await assert.rejects(runtime.broker.request(cap, { verb: 'library.file', args: { project: 'books', path: GET.path } }),
{ code: 'human-required' });
});
+43
View File
@@ -0,0 +1,43 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { Broker } from '../src/broker.mjs';
import { Store } from '../src/store.mjs';
import { citationWorld, citedBody, fixtureCite } from './citation-helpers.mjs';
test('C2-10c room.read carries exactly the thread check and current file last', async t => {
const f = citationWorld(t, { definition: 'companion' });
const room = f.call(f.human, 'room.open', { project: f.store.get('SELECT id FROM projects').id });
const thread = room.threads[0].id;
const agent = f.broker.bindLaunch({ ...f.launch, run: 'room-agent', thread });
f.call(agent, 'role.claim');
const sent = await f.call(agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) });
const [single] = await f.call(f.human, 'thread.messages', { thread });
const merged = await f.call(f.human, 'room.read', { project: room.project });
const same = merged.find(m => m.id === sent.id);
assert.deepEqual(same.check, single.check);
assert.deepEqual(same.check.citations, [{ scope: 'canon', agent: null, file: f.commit, last: f.commit }]);
assert.equal(f.reads.length, 2, 'both read verbs route through last hydration');
});
test('C2-10c check hydration preserves unchecked rows and one broadcast with two addressees', t => {
const root = mkdtempSync(join(tmpdir(), 'room-check-')), store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const messaging = JSON.parse(readFileSync(new URL('../../../roles/companion.json', import.meta.url))).messaging;
const role = { definition: 'companion', authority: { withinRole: ['message.send'], crossRole: [] } };
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', vars: {},
arbiters: { technical: 'bean', delivery: 'bean' }, roles: { bean: role, chet: role }, definitions: { companion: { messaging } } } } });
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const call = (verb, args) => broker.request(human, { verb, args });
const project = call('project.create', { name: 'Room' }).id;
call('room.open', { project });
const receipt = call('room.send', { project, body: 'One broadcast.' });
const rows = call('room.read', { project });
assert.equal(rows.length, 1);
assert.deepEqual(rows[0].ids, receipt.ids);
assert.deepEqual(rows[0].to, ['bean', 'chet']);
assert.equal(Object.hasOwn(rows[0], 'check'), false);
assert.equal(rows[0].body, 'One broadcast.');
});
+1
View File
@@ -528,6 +528,7 @@ export function createLauncher({
contract: readFileSync(resolved.contract, "utf8"),
arbiter: Object.values(b.arbiters).includes(instance),
tracker,
library: Object.values(b.projects ?? {}).some(project => project.kind === "library"),
credentials,
session: { run, launchedBy, workspace: inSandbox, toolSocket, turnMarker, checkouts: inSandbox === workspace ? checkouts : checkouts.map((c) => ({ ...c, path: join(inSandbox, relative(workspace, c.path)) })) },
harness,
+27
View File
@@ -27,6 +27,7 @@ import { bootConfig, loadSystem } from "../src/config.mjs";
import { startTimeOf } from "../src/host.mjs";
import { createLauncher } from "../src/launcher.mjs";
import { fakeHarnesses, fixture, tmp } from "./helpers.mjs";
import { mappingEntry } from "../../library/tests/helpers.mjs";
// Row 264a (#1769): a thread made by hand gets the default session the 3k-3l migration gives a
// legacy thread, with the thread's id.
const DEFAULT_SESSION = [
@@ -533,3 +534,29 @@ test('C2-09a: terminating one creative thread does not signal an unselected peer
await until(() => !readSessions(f.dataRoot).some(row => row.run === runs[0]));
assert.ok(readSessions(f.dataRoot).some(row => row.run === runs[1]));
});
// Row224: the launcher must derive library presence from business projects, not
// from session checkouts (a library never has an agent clone).
test("C2-05b a real launched bundle gets lookups for a library project without a library checkout", { skip, timeout: 60000 }, async t => {
const prepared = prepare(t, { idle: 10, edit: doc => {
doc.projects.books = mappingEntry();
doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } };
return doc;
} });
const { f } = prepared;
const host = await start(t, prepared);
const project = host.call("project.create", { name: "library work", repo: "stack" }).id;
const thread = host.call("thread.create", { project, name: "lookup" }).id;
const sent = host.call("thread.send", { thread, body: "inspect bundle" });
// broker.mjs:4854 uses async citation hydration for a business with a library.
const reply = await until(async () => (await host.call("thread.messages", { thread })).find(m => m.in_reply_to === sent.id && m.class === "RESULT"));
assert.equal(reply.from_role, "assistant");
assert.equal(reply.to_role, "human");
const run = launchLog(f).find(e => e.event === "launch" && e.thread === thread).run;
const tools = JSON.parse(readFileSync(join(f.dataRoot, "launches", "acme", run, "bundle", "tools.json"), "utf8"));
assert.deepEqual(tools.filter(tool => tool.name.startsWith("library_")).map(tool => tool.name),
["library_find", "library_get", "library_search", "library_manuscript", "library_universe"]);
const [session] = threadRuns(f, thread);
assert.ok(session);
assert.ok(!session.checkouts?.some(checkout => checkout.project === "books"));
});
+3 -2
View File
@@ -292,6 +292,7 @@ function prompt({ contract, resolved, session, tools, untyped, builtins, persona
// resolved resolveInstance(...) output
// contract the role contract's text
// arbiter true when the business names this instance as an arbiter
// library true when the trusted business configuration has a library project
// tracker true when the broker has task verbs for the business
// credentials the broker's credentialStatus(...) for the instance (metadata only)
// session { run, launchedBy, workspace, toolSocket, turnMarker, checkouts }
@@ -309,11 +310,11 @@ function prompt({ contract, resolved, session, tools, untyped, builtins, persona
// (row 213); an instance with persona.sha256 and no personaDir refuses
// Returns { manifest, files, policy, tools, env } with absolute paths; env is the module's
// adapter variables, which the host writes into session.json.
export function buildBundle({ dir, resolved, contract, arbiter = false, tracker = false, credentials = [], session, harness, route = "primary", routeReason = null, harnessVersion = null, models = null, land = false, skillsDir = null, personaDir = null }) {
export function buildBundle({ dir, resolved, contract, arbiter = false, tracker = false, library = false, credentials = [], session, harness, route = "primary", routeReason = null, harnessVersion = null, models = null, land = false, skillsDir = null, personaDir = null }) {
if (route !== "primary" && route !== "fallback") throw new BundleError(`route ${JSON.stringify(route)} isn't primary or fallback`);
const model = sessionModel(resolved, models, route);
const web = webPlan(resolved, model.harness, harness.builtinWeb());
const tools = typedTools({ resolved, arbiter, tracker, projects: (session.checkouts ?? []).map((c) => c.project), webTyped: !web.builtin, land });
const tools = typedTools({ resolved, arbiter, tracker, library, projects: (session.checkouts ?? []).map((c) => c.project), webTyped: !web.builtin, land });
const untyped = untypedActions(resolved, tools);
const persona = readPersona(resolved, personaDir);
mkdirSync(dir, { recursive: true, mode: 0o700 });
+34 -2
View File
@@ -78,9 +78,10 @@ const TASK_TOOLS = {
const MESSAGE_CLASSES = ["REQUEST", "ASSIGNMENT", "REVIEW-REQUEST", "REVIEW-RESULT", "RESULT", "INFO", "DECISION", "REACTION", "BLOCK"];
// typedTools({ resolved, arbiter, tracker, projects, land }) -> [{ name, verb, action, description, parameters }]
// typedTools({ resolved, arbiter, tracker, projects, land, library }) -> [{ name, verb, action, description, parameters }]
// resolved resolveInstance(...) output for the session's instance
// arbiter true when the business names this instance as an arbiter
// library true when the trusted business configuration has a library project
// tracker true when the broker has task verbs for the business
// projects the project ids the session has a clone of
// land true when the role and thread hold a merge decision a landing could use (landHeld)
@@ -88,7 +89,7 @@ const MESSAGE_CLASSES = ["REQUEST", "ASSIGNMENT", "REVIEW-REQUEST", "REVIEW-RESU
// `verb` is the broker verb; the `launch` tool has verb "launch" and goes
// to the host instead, and "working.push", "working.fetch" and
// "working.land" go to the host's git socket.
export function typedTools({ resolved, arbiter = false, tracker = false, projects = [], webTyped = true, land = false }) {
export function typedTools({ resolved, arbiter = false, tracker = false, projects = [], webTyped = true, land = false, library = false }) {
const held = new Set([...resolved.limits.authority.withinRole, ...resolved.limits.authority.crossRole]);
const tools = [];
const add = (name, verb, action, description, parameters) => tools.push({ name, verb, action, description, parameters });
@@ -222,6 +223,32 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
url: str("The https url to fetch", { maxLength: 2048 }),
}, ["url"]));
}
// Row224: no library clone or token reaches a session. The broker chooses the served
// commit and requires a universe on every call; cursors page without cutting replies.
if (library) {
const universe = str("Universe ID in the configured library", { minLength: 1 });
const project = str("Library project ID; omit only when the business has one library");
const scope = str("canon by default; playground is opt-in", { enum: ["canon", "playground"] });
const book = str("Book folder from library_universe");
const offset = { type: "integer", minimum: 0, description: "next from the previous page" };
const line = { type: "integer", minimum: 1, description: "One-based line number" };
const base = { universe, project };
add("library_find", "library.find", null, "Find names in one universe, at the served commit. At most 50 results; continue with next as offset.", obj({
...base, query: str("Case-insensitive name or filename substring; empty matches all"),
type: str("Mapped type", { enum: ["character", "place", "research", "tracker", "summary"] }),
book, scope, offset,
}, ["universe", "query"]));
add("library_get", "library.get", null, "Read a mapped note and visible links in one universe at the served commit. next pages lines; links_next pages links. An explicit own-universe playground path is readable.", obj({
...base, path: str("Mapped note path relative to the library"), from: line, links_from: offset,
}, ["universe", "path"]));
add("library_search", "library.search", null, "Search text in one universe at the served commit. At most 50 hits; use next as offset. Playground is opt-in.", obj({
...base, text: str("Nonempty case-insensitive substring", { minLength: 1 }), book, scope, offset,
}, ["universe", "text"]));
add("library_manuscript", "library.manuscript", null, "Read one chapter of a book at the served commit, at most 400 lines. Omit chapter for the first; continue with next or next_chapter.", obj({
...base, book, scope, chapter: str("Chapter filename without extension"), from: line, to: line,
}, ["universe", "book"]));
add("library_universe", "library.universe", null, "Read a universe's mapping at the served commit: books, types, playground prefixes, and hybrid sources/constraints/notes or draft metadata.", obj(base, ["universe"]));
}
add("read_spec", "spec.read", null, "Read a project's committed north-star spec and its file history. commit is an optional full reachable commit ID. The file commit, rather than HEAD, is what a decision cites. confirmed says the file commit was authored by the business's human.", obj({
project: str("Project ID from the business file"),
commit: str("Optional full historical commit ID", { pattern: "^[0-9a-f]{40}$" }),
@@ -277,6 +304,11 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
if (held.has("task.create"))
add("list_requirements", "requirements", null, "List the approved PRD's requirement ids, each with a one-line title. task_create takes only these. When none fits a request, raise a prd.approve decision instead of creating the task.", obj({}));
}
// mos-1308 / M3: no generic spec, evidence, tracker, decision or write tools. The
// C2-08 web grant still supplies broker web reads to companion and research only.
if (CREATIVE_DEFINITIONS.includes(resolved.definition))
return tools.filter(tool => tool.name === "send_message" || tool.name.startsWith("library_") ||
tool.name === "web_search" || tool.name === "web_fetch");
return tools;
}
@@ -0,0 +1,71 @@
// Row224 M3: exact creative tool sets preserve the existing C2-08 web grants.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { typedTools } from '../src/tools.mjs';
import { buildBundle } from '../src/bundle.mjs';
import { TYPES } from '../../library/src/mapping.mjs';
import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs';
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
const CREATIVE = ['companion', 'research', 'ideas', 'proofreader', 'fact-checker', 'continuity'];
const role = (t, definition, harness = 'pi') => resolvedFor(t, 'bean', d => {
d.roles.bean = { definition, credentials: { gitea: { ...d.roles.coder.credentials.gitea } }, vars: { harness, model: 'claude-sonnet-5-5', thinking: 'high' } };
});
const expected = definition => ['send_message', ...(['companion', 'research'].includes(definition)
? ['web_search', 'web_fetch'] : []), ...LOOKUPS];
test('M3: per-role snapshots contain six names, or eight with the C2-08 web grant', t => {
for (const definition of CREATIVE) {
const tools = typedTools({ resolved: role(t, definition), library: true, tracker: true, arbiter: true });
assert.deepEqual(tools.map(t => t.name), expected(definition), definition);
assert.ok(tools.every(t => t.parameters.additionalProperties === false));
}
});
test('every noncreative role gets lookups when a library is configured, not otherwise', t => {
for (const instance of ['coder', 'reviewer', 'pm', 'chief']) {
const resolved = resolvedFor(t, instance);
const ordinary = typedTools({ resolved }).map(t => t.name);
assert.ok(!ordinary.some(name => name.startsWith('library_')));
const tools = typedTools({ resolved, library: true });
assert.deepEqual(tools.filter(t => t.name.startsWith('library_')).map(t => t.name), LOOKUPS);
assert.deepEqual(tools.filter(t => !t.name.startsWith('library_')).map(t => t.name), ordinary);
}
});
test('lookup schemas require a universe even for get, include paging, and cannot select repo or commit', t => {
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
const required = [ ['universe', 'query'], ['universe', 'path'], ['universe', 'text'], ['universe', 'book'], ['universe'] ];
for (const [i, tool] of tools.filter(t => t.name.startsWith('library_')).entries()) {
assert.deepEqual(tool.parameters.required, required[i]);
assert.equal(tool.verb, LOOKUPS[i].replace('_', '.'));
assert.equal(tool.action, null);
assert.equal(tool.parameters.additionalProperties, false);
for (const key of ['repo', 'commit', 'business', 'accept']) assert.equal(Object.hasOwn(tool.parameters.properties, key), false);
}
const byName = Object.fromEntries(tools.map(t => [t.name, t]));
assert.deepEqual(byName.library_find.parameters.properties.scope.enum, ['canon', 'playground']);
assert.deepEqual(byName.library_find.parameters.properties.type.enum, [...TYPES]);
assert.equal(byName.library_find.parameters.properties.offset.minimum, 0);
assert.equal(byName.library_get.parameters.properties.from.minimum, 1);
assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0);
assert.equal(byName.library_manuscript.parameters.properties.chapter.type, 'string');
});
test('bundle flag reaches both harness tool manifests and creative prompt without generic tools', t => {
for (const definition of CREATIVE) for (const harness of ['pi', 'claude-code']) {
const dir = scratch(t), workspace = join(dir, 'ws'); mkdirSync(workspace);
const b = buildBundle({ dir: join(dir, 'bundle'), resolved: role(t, definition, harness), library: true,
contract: '# Creative\n', tracker: true, arbiter: true, skillsDir: REPO_SKILLS,
session: { run: 'r224', launchedBy: 'chief', workspace, toolSocket: join(dir, 't.sock'), turnMarker: join(dir, 'turn.done') },
harness: HARNESS[harness] });
assert.deepEqual(b.tools.map(t => t.name), expected(definition), `${definition} ${harness}`);
const serialized = JSON.parse(readFileSync(b.files.tools, 'utf8'));
assert.deepEqual(serialized.map(t => t.name), expected(definition));
const prompt = readFileSync(b.files.prompt, 'utf8');
assert.match(prompt, /library_find/);
assert.doesNotMatch(prompt, /Bus tools: [^\n]*(read_spec|list_agents|inbox|trail|write_record|raise_decision|list_tasks)/);
}
});
+3
View File
@@ -2,6 +2,9 @@
import { PluginError } from "../../src/kinds.mjs";
export const interfaceVersion = 1;
const VERBS = new Set([
"room.open",
"room.read",
"room.send",
"project.list",
"project.create",
"thread.list",
@@ -0,0 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { dispatch } from '../channel/webui/index.mjs';
test('C2-10c webui permits exactly the three added room verbs and preserves core refusals', async () => {
for (const verb of ['room.open', 'room.read', 'room.send']) {
const calls = [], args = { project: 'p', body: 'typed @name' };
let receipt;
await assert.doesNotReject(async () => { receipt = await dispatch({ verb, args }, { call: async (...a) => { calls.push(a); return { ok: verb }; } }); });
assert.deepEqual(receipt, { ok: verb });
assert.deepEqual(calls, [[verb, args]]);
const refused = Object.assign(new Error('refused'), { code: 'unknown-addressee' });
await assert.rejects(dispatch({ verb, args }, { call: async () => { throw refused; } }), e => e === refused);
}
for (const verb of ['room.delete', 'room.members', 'message.send', 'role.claim', 'shell.exec']) {
let called = false;
await assert.rejects(dispatch({ verb, args: {} }, { call: async () => { called = true; } }), { code: 'channel-operation' });
assert.equal(called, false);
}
});
+80
View File
@@ -0,0 +1,80 @@
# Supervisor
Q198 S1 (row 257, #1762): parsers for the host pressure inputs in design D4
(`docs/plans/2026-10-21_q198-system-manager.md`), and a pure evaluator that turns timestamped samples
into a hold or release decision. Nothing here samples the host, runs a process or timer, or enforces
anything. The live sampler, the Console view and admission are S2 and S3.
```sh
node --test packages/supervisor/tests/
```
## Parsers
Each parser takes file text the caller already read. Text that isn't the shape the kernel writes throws
a `SupervisorError` with `kind: "parse"`; a sampler treats that as a missing signal.
| Export | Input | Returns |
|---|---|---|
| `parseLoadavg` | `/proc/loadavg` | `{ load1, load5, load15 }` |
| `parseMeminfo` | `/proc/meminfo` | `{ memTotal, memAvailable, swapTotal, swapFree }` in bytes |
| `parsePsi(text, resource)` | `/proc/pressure/{cpu,memory,io}` or a cgroup `*.pressure` | `{ some, full }`, each `{ avg10, avg60, avg300, total }`; `full` is null only for cpu |
| `parseCpuList` | `/sys/devices/system/cpu/online`, `cpuset.cpus.effective` | how many CPUs the list names |
| `parseCpuMax` | `cpu.max` | the quota in CPUs, or null for `max` |
| `parseMemoryMax` | `memory.max` | bytes, or null for `max` |
| `parseMemoryCurrent` | `memory.current` | bytes |
| `parseMemoryEvents` | `memory.events` | `{ oom, oomKill }` |
| `parseVmstat` | `/proc/vmstat` | `{ pswpin, pswpout, oomKill }`; `oomKill` is the host-only OOM counter, null when absent |
`effectiveCpus({ online, cpuset, cpuMax })` is the smallest of the host count, the cpuset and the quota
at every cgroup level given; a fractional quota stays fractional. `effectiveMemory({ meminfo, levels })`
takes cgroup levels as `{ max, current }` text. The limit is the smallest of `MemTotal` and every numeric
`memory.max`. Available memory is the smallest of `MemAvailable` and `max - current` at each limited level.
`memory.current` counts page cache, so this errs toward holding. With no cgroup input both functions use
the host's figures.
## Evaluator
`evaluate(state, sample, rules, { reconciled })` returns `{ state, decision }`. It starts from
`initialState()`, which is held: no signal has been seen yet. Time is the sample's `at` in milliseconds.
The evaluator never reads the clock.
A sample has these signals: `load { at, load1, cpus }`, `memory { at, available, limit }`,
`psi { at, cpuSome, memoryFull, ioFull }` (avg10 percentages), `oom { at, kills }` and `disk { at, free }`.
| Signal | Hold | Release |
|---|---|---|
| `load1 / cpus` | ≥ 0.80 on 3 consecutive samples | ≤ 0.55 |
| available memory | < max(15% of limit, 1 GiB), on one sample | > max(25% of limit, 2 GiB) |
| PSI avg10 | cpu some ≥ 20, memory full ≥ 2 or io full ≥ 10, on 3 consecutive samples | cpu some < 10, memory full < 0.5 and io full < 3 |
| critical | OOM kill counter rises, memory < 5% of limit, or disk free < 1 GiB | the caller passes `reconciled`, then the normal release |
- **Release** needs every applicable release condition on every sample for 30 s. A dead-band
sample (between hold and release) or a new hold restarts the window.
- **Missing or stale:** a required signal that is missing, malformed, more than 6 s older than the
sample, or later than it holds at once with `health-unknown`. `load`, `memory`, `oom` and `disk` are
always required. PSI becomes required once a sample has carried it. A host without PSI skips the PSI
rows, and one that loses it holds.
- **Consecutive counts** reset after a gap of more than 6 s between samples. The PSI count runs across
resources: any PSI hold condition on 3 consecutive samples holds.
- **OOM:** the first OOM sample is the baseline. A counter that falls (a recreated cgroup) resets the
baseline with the note `oom-counter-reset`.
- **Critical** never ends on thresholds alone. `reconciled` only counts once no critical condition
remains, and the 30 s release still follows. The evaluator never sets it and never touches a kill
switch.
- **A clock that goes backwards** holds with `health-unknown` and the note `clock-backwards`.
`decision` is `{ mode, reasons, unknown, critical, releaseAt, notes }`.
- `mode` is `open`, `held` or `critical`.
- `reasons` are `health-unknown`, `load-high`, `memory-low`, `psi-cpu`, `psi-memory`, `psi-io`,
`hysteresis`, `release-pending` or the critical codes `critical-oom`, `critical-memory` and
`critical-disk`.
- `releaseAt` is set while a release is pending.
`DEFAULT_RULES` is the D4 table, frozen. `resolveRules(overrides)` merges overrides key by key. It refuses
with `kind: "rules"` on an unknown key, a non-number or a contradictory pair, for example:
- a release threshold at or past its hold;
- a release floor at or below its hold floor;
- a critical memory fraction at or above the hold fraction;
- a stale limit at or below the sample interval.
Provider scope (429, quota, Retry-After) is not here; it belongs to S3 and S5.
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@mosaic/supervisor",
"version": "0.1.0",
"private": true,
"description": "Q198 S1: parsers for host pressure inputs and a pure hold and release rules evaluator. No sampler, process or enforcement.",
"license": "UNLICENSED",
"type": "module",
"engines": { "node": ">=24" },
"exports": { ".": "./src/index.mjs" },
"scripts": { "test": "node --test tests/" }
}
+10
View File
@@ -0,0 +1,10 @@
// Every refusal in this package is a SupervisorError. `kind` is "parse" for
// input text that isn't the shape the kernel writes, and "rules" for settings
// the evaluator refuses.
export class SupervisorError extends Error {
constructor(message, kind) {
super(message);
this.name = "SupervisorError";
this.kind = kind;
}
}
+10
View File
@@ -0,0 +1,10 @@
// @mosaic/supervisor: Q198 S1. Parsers for host pressure inputs and a pure
// rules evaluator. Nothing here reads a file, the clock or the environment,
// and nothing enforces: no sampler, process, timer, bus or Console (S2, S3).
export { SupervisorError } from "./errors.mjs";
export {
parseLoadavg, parseMeminfo, parsePsi, parseCpuList, parseCpuMax, parseMemoryMax, parseMemoryCurrent,
parseMemoryEvents, parseVmstat, effectiveCpus, effectiveMemory,
} from "./pressure.mjs";
export { DEFAULT_RULES, resolveRules, initialState, evaluate } from "./rules.mjs";
+165
View File
@@ -0,0 +1,165 @@
// Parsers for the host pressure inputs Q198 D4 names, and the effective CPU
// count and memory limit they give. Every function takes file text the caller
// already read and returns numbers. Nothing here opens a file. Text that isn't
// the shape the kernel writes throws a SupervisorError of kind "parse"; the
// caller turns that into a missing signal.
import { SupervisorError } from "./errors.mjs";
const fail = (what, why) => { throw new SupervisorError(`${what}: ${why}`, "parse"); };
function text(what, value) {
if (typeof value !== "string") fail(what, "expected file text");
return value.endsWith("\n") ? value.slice(0, -1) : value;
}
function integer(what, value) {
if (!/^\d+$/.test(value)) fail(what, `not a non-negative integer: ${JSON.stringify(value)}`);
return Number(value);
}
function decimal(what, value) {
if (!/^\d+(\.\d+)?$/.test(value)) fail(what, `not a non-negative number: ${JSON.stringify(value)}`);
return Number(value);
}
// "key value" lines (memory.events, /proc/vmstat). A repeated key throws.
function keyedIntegers(what, raw) {
const out = new Map();
for (const line of text(what, raw).split("\n")) {
const m = /^([a-z0-9_]+) (\d+)$/.exec(line);
if (!m) fail(what, `bad line ${JSON.stringify(line)}`);
if (out.has(m[1])) fail(what, `repeated key ${m[1]}`);
out.set(m[1], integer(what, m[2]));
}
return out;
}
// /proc/loadavg: "0.52 0.58 0.59 1/1234 56789".
export function parseLoadavg(raw) {
const m = /^(\d+\.\d+) (\d+\.\d+) (\d+\.\d+) (\d+)\/(\d+) (\d+)$/.exec(text("loadavg", raw));
if (!m) fail("loadavg", "not the /proc/loadavg shape");
return { load1: Number(m[1]), load5: Number(m[2]), load15: Number(m[3]) };
}
// /proc/meminfo. Values are returned in bytes; lines without a unit (the
// HugePages counts) are kept as plain counts.
export function parseMeminfo(raw) {
const fields = new Map();
for (const line of text("meminfo", raw).split("\n")) {
const m = /^([A-Za-z0-9_()]+):\s+(\d+)( kB)?$/.exec(line);
if (!m) fail("meminfo", `bad line ${JSON.stringify(line)}`);
if (fields.has(m[1])) fail("meminfo", `repeated key ${m[1]}`);
fields.set(m[1], integer("meminfo", m[2]) * (m[3] ? 1024 : 1));
}
for (const key of ["MemTotal", "MemAvailable"]) if (!fields.has(key)) fail("meminfo", `no ${key}`);
return {
memTotal: fields.get("MemTotal"),
memAvailable: fields.get("MemAvailable"),
swapTotal: fields.get("SwapTotal") ?? null,
swapFree: fields.get("SwapFree") ?? null,
};
}
// /proc/pressure/{cpu,memory,io} or a cgroup's *.pressure file. The cpu file
// may lack the "full" line on older kernels; memory and io must have it.
export function parsePsi(raw, resource) {
if (!["cpu", "memory", "io"].includes(resource)) fail("psi", `unknown resource ${JSON.stringify(resource)}`);
const what = `psi ${resource}`;
const out = { some: null, full: null };
for (const line of text(what, raw).split("\n")) {
const m = /^(some|full) avg10=(\S+) avg60=(\S+) avg300=(\S+) total=(\S+)$/.exec(line);
if (!m) fail(what, `bad line ${JSON.stringify(line)}`);
if (out[m[1]]) fail(what, `repeated ${m[1]} line`);
out[m[1]] = { avg10: decimal(what, m[2]), avg60: decimal(what, m[3]), avg300: decimal(what, m[4]), total: integer(what, m[5]) };
}
if (!out.some) fail(what, "no some line");
if (!out.full && resource !== "cpu") fail(what, "no full line");
return out;
}
// A CPU list such as /sys/devices/system/cpu/online or cpuset.cpus.effective:
// "0-3,6,8-9". Returns how many distinct CPUs it names.
export function parseCpuList(raw) {
const body = text("cpu list", raw);
if (body === "") fail("cpu list", "empty");
const cpus = new Set();
for (const part of body.split(",")) {
const m = /^(\d+)(?:-(\d+))?$/.exec(part);
if (!m) fail("cpu list", `bad range ${JSON.stringify(part)}`);
const lo = integer("cpu list", m[1]);
const hi = m[2] === undefined ? lo : integer("cpu list", m[2]);
if (hi < lo) fail("cpu list", `range ${part} runs backwards`);
for (let c = lo; c <= hi; c++) cpus.add(c);
}
return cpus.size;
}
// cgroup v2 cpu.max: "max 100000" (no quota) or "50000 100000". Returns the
// CPUs the quota allows, or null for no quota.
export function parseCpuMax(raw) {
const m = /^(max|\d+) (\d+)$/.exec(text("cpu.max", raw));
if (!m) fail("cpu.max", "not the cpu.max shape");
const period = integer("cpu.max", m[2]);
if (period === 0) fail("cpu.max", "period is 0");
return m[1] === "max" ? null : integer("cpu.max", m[1]) / period;
}
// cgroup v2 memory.max: "max" (no limit) or bytes. Returns null for "max".
export function parseMemoryMax(raw) {
const body = text("memory.max", raw);
return body === "max" ? null : integer("memory.max", body);
}
// cgroup v2 memory.current: bytes.
export function parseMemoryCurrent(raw) {
return integer("memory.current", text("memory.current", raw));
}
// cgroup v2 memory.events. oom_kill is the counter the critical row watches.
export function parseMemoryEvents(raw) {
const fields = keyedIntegers("memory.events", raw);
if (!fields.has("oom_kill")) fail("memory.events", "no oom_kill");
return { oom: fields.get("oom") ?? null, oomKill: fields.get("oom_kill") };
}
// /proc/vmstat. Swap activity is reported only (D4 has no threshold for it).
// oom_kill is the host-only OOM counter; it is null on a kernel without it.
export function parseVmstat(raw) {
const fields = keyedIntegers("vmstat", raw);
for (const key of ["pswpin", "pswpout"]) if (!fields.has(key)) fail("vmstat", `no ${key}`);
return { pswpin: fields.get("pswpin"), pswpout: fields.get("pswpout"), oomKill: fields.get("oom_kill") ?? null };
}
// Effective CPUs: the smallest of the host's online CPUs, the cpuset and the
// quota at every cgroup level given. With no cgroup input it is the host count.
// A fractional quota stays fractional.
export function effectiveCpus({ online, cpuset = null, cpuMax = [] }) {
let cpus = parseCpuList(online);
if (cpuset !== null) cpus = Math.min(cpus, parseCpuList(cpuset));
for (const level of cpuMax) {
const quota = parseCpuMax(level);
if (quota !== null) cpus = Math.min(cpus, quota);
}
return cpus;
}
// Effective memory. `levels` lists the cgroup levels that apply, each
// { max, current } as file text. The limit is the smallest of MemTotal and every
// numeric memory.max. Available memory is the smallest of MemAvailable and
// (max - current) at every limited level. memory.current counts page cache,
// so this errs toward holding. With no levels it is the host's figures.
export function effectiveMemory({ meminfo, levels = [] }) {
const host = parseMeminfo(meminfo);
let limit = host.memTotal;
let available = host.memAvailable;
for (const level of levels) {
const max = parseMemoryMax(level.max);
if (max === null) continue;
if (level.current === undefined || level.current === null) fail("memory.current", "missing for a limited level");
const current = parseMemoryCurrent(level.current);
limit = Math.min(limit, max);
available = Math.min(available, Math.max(0, max - current));
}
return { limit, available };
}
+201
View File
@@ -0,0 +1,201 @@
// The Q198 D4 hold and release rules as a pure evaluator. evaluate() takes the
// previous state, one timestamped sample and the rules, and returns the next
// state and a decision. It never reads the clock, a file or the environment:
// time is the sample's `at`, in milliseconds. Nothing here enforces anything;
// the decision is advice for S2 and S3.
import { SupervisorError } from "./errors.mjs";
const GiB = 1024 ** 3;
// The D4 table. Defaults are the design's proposals, not measured limits.
export const DEFAULT_RULES = deepFreeze({
sampleMs: 2000,
staleMs: 6000,
releaseMs: 30000,
load: { hold: 0.8, release: 0.55, samples: 3 },
memory: { holdFraction: 0.15, holdBytes: 1 * GiB, releaseFraction: 0.25, releaseBytes: 2 * GiB },
psi: {
samples: 3,
hold: { cpuSome: 20, memoryFull: 2, ioFull: 10 },
release: { cpuSome: 10, memoryFull: 0.5, ioFull: 3 },
},
critical: { memoryFraction: 0.05, diskBytes: 1 * GiB },
});
function deepFreeze(o) {
for (const v of Object.values(o)) if (v && typeof v === "object") deepFreeze(v);
return Object.freeze(o);
}
const refuse = (why) => { throw new SupervisorError(`rules: ${why}`, "rules"); };
// Overrides merge onto the defaults key by key. An unknown key, a value of the
// wrong type or a contradictory pair refuses.
function merge(base, over, at) {
if (over === undefined) return structuredClone(base);
if (!over || typeof over !== "object" || Array.isArray(over)) refuse(`${at || "rules"} must be an object`);
const out = {};
for (const key of Object.keys(over)) if (!Object.hasOwn(base, key)) refuse(`unknown key ${at ? `${at}.` : ""}${key}`);
for (const [key, value] of Object.entries(base)) {
const name = at ? `${at}.${key}` : key;
if (typeof value === "object") out[key] = merge(value, over[key], name);
else {
const v = over[key] ?? value;
if (typeof v !== "number" || !Number.isFinite(v)) refuse(`${name} must be a finite number`);
out[key] = v;
}
}
return out;
}
export function resolveRules(overrides = {}) {
const r = merge(DEFAULT_RULES, overrides, "");
const check = (ok, why) => { if (!ok) refuse(why); };
const fraction = (v) => v > 0 && v < 1;
const percent = (v) => v >= 0 && v <= 100;
check(r.sampleMs > 0, "sampleMs must be above 0");
check(r.staleMs > r.sampleMs, "staleMs must be above sampleMs");
check(r.releaseMs >= 0, "releaseMs must not be negative");
for (const k of ["load", "psi"]) check(Number.isInteger(r[k].samples) && r[k].samples >= 1, `${k}.samples must be an integer of at least 1`);
check(r.load.release > 0 && r.load.release < r.load.hold, "load.release must be above 0 and below load.hold");
check(fraction(r.memory.holdFraction) && fraction(r.memory.releaseFraction), "memory fractions must be between 0 and 1");
check(r.memory.releaseFraction > r.memory.holdFraction, "memory.releaseFraction must be above memory.holdFraction");
check(r.memory.holdBytes >= 0 && r.memory.releaseBytes > r.memory.holdBytes, "memory.releaseBytes must be above memory.holdBytes");
for (const k of ["cpuSome", "memoryFull", "ioFull"]) {
check(percent(r.psi.hold[k]) && percent(r.psi.release[k]), `psi ${k} values must be percentages`);
check(r.psi.release[k] > 0 && r.psi.release[k] < r.psi.hold[k], `psi.release.${k} must be above 0 and below psi.hold.${k}`);
}
check(fraction(r.critical.memoryFraction) && r.critical.memoryFraction < r.memory.holdFraction,
"critical.memoryFraction must be between 0 and memory.holdFraction");
check(r.critical.diskBytes > 0, "critical.diskBytes must be above 0");
return deepFreeze(r);
}
// The state before any sample: held, because no signal has been seen yet.
export function initialState() {
return { mode: "held", lastAt: null, loadCount: 0, psiCount: 0, psiSeen: false, oomBaseline: null, releaseSince: null, critical: [] };
}
const SIGNAL_FIELDS = {
load: ["load1", "cpus"],
memory: ["available", "limit"],
psi: ["cpuSome", "memoryFull", "ioFull"],
oom: ["kills"],
disk: ["free"],
};
// A signal is usable when every field is a non-negative finite number, cpus
// and limit are above 0, and its own timestamp is no later than the sample and
// no more than staleMs before it.
function fresh(name, signal, now, rules) {
if (!signal || typeof signal !== "object") return false;
if (typeof signal.at !== "number" || !Number.isFinite(signal.at)) return false;
if (signal.at > now || now - signal.at > rules.staleMs) return false;
for (const f of SIGNAL_FIELDS[name]) if (typeof signal[f] !== "number" || !Number.isFinite(signal[f]) || signal[f] < 0) return false;
if (name === "load" && signal.cpus <= 0) return false;
if (name === "memory" && signal.limit <= 0) return false;
return true;
}
// Sample: { at, load: { at, load1, cpus }, memory: { at, available, limit },
// psi: { at, cpuSome, memoryFull, ioFull } (avg10 percentages),
// oom: { at, kills }, disk: { at, free } }. `reconciled` is the caller's word
// that reconciliation finished; only it lets a critical hold end.
export function evaluate(state, sample, rules = DEFAULT_RULES, { reconciled = false } = {}) {
if (!sample || typeof sample.at !== "number" || !Number.isFinite(sample.at)) {
throw new SupervisorError("sample: at must be a finite number of milliseconds", "sample");
}
const now = sample.at;
const next = { ...state, critical: [...state.critical], lastAt: now };
const unknown = [];
const notes = [];
const gap = state.lastAt === null ? Infinity : now - state.lastAt;
// A count of consecutive samples survives only a gap of at most staleMs.
const contiguous = gap >= 0 && gap <= rules.staleMs;
if (gap < 0) { unknown.push("clock"); notes.push("clock-backwards"); }
let loadHold = false, loadReleases = false;
if (fresh("load", sample.load, now, rules)) {
const ratio = sample.load.load1 / sample.load.cpus;
next.loadCount = ratio >= rules.load.hold ? (contiguous ? state.loadCount : 0) + 1 : 0;
loadHold = next.loadCount >= rules.load.samples;
loadReleases = ratio <= rules.load.release;
} else { unknown.push("load"); next.loadCount = 0; }
let memoryLow = false, memoryReleases = false, criticalMemory = false;
if (fresh("memory", sample.memory, now, rules)) {
const { available, limit } = sample.memory;
const m = rules.memory;
// "Whichever demands more free memory": the larger floor applies.
memoryLow = available < Math.max(m.holdFraction * limit, m.holdBytes);
memoryReleases = available > Math.max(m.releaseFraction * limit, m.releaseBytes);
criticalMemory = available < rules.critical.memoryFraction * limit;
} else unknown.push("memory");
// PSI is required once it has been seen. A host that never had it skips the
// PSI rows; one that loses it holds with health-unknown.
let psiHold = false, psiReleases = true;
const psiOver = [];
if (sample.psi !== undefined && sample.psi !== null) next.psiSeen = true;
if (fresh("psi", sample.psi, now, rules)) {
const p = sample.psi, h = rules.psi.hold, r = rules.psi.release;
if (p.cpuSome >= h.cpuSome) psiOver.push("psi-cpu");
if (p.memoryFull >= h.memoryFull) psiOver.push("psi-memory");
if (p.ioFull >= h.ioFull) psiOver.push("psi-io");
next.psiCount = psiOver.length ? (contiguous ? state.psiCount : 0) + 1 : 0;
psiHold = next.psiCount >= rules.psi.samples;
psiReleases = p.cpuSome < r.cpuSome && p.memoryFull < r.memoryFull && p.ioFull < r.ioFull;
} else {
next.psiCount = 0;
if (next.psiSeen) { unknown.push("psi"); psiReleases = false; }
}
let criticalOom = false;
if (fresh("oom", sample.oom, now, rules)) {
const kills = sample.oom.kills;
if (state.oomBaseline !== null && kills > state.oomBaseline) criticalOom = true;
if (state.oomBaseline !== null && kills < state.oomBaseline) notes.push("oom-counter-reset");
next.oomBaseline = kills;
} else unknown.push("oom");
let criticalDisk = false;
if (fresh("disk", sample.disk, now, rules)) criticalDisk = sample.disk.free < rules.critical.diskBytes;
else unknown.push("disk");
const criticalNow = [];
if (criticalOom) criticalNow.push("critical-oom");
if (criticalMemory) criticalNow.push("critical-memory");
if (criticalDisk) criticalNow.push("critical-disk");
const decide = (mode, reasons, releaseAt = null) => {
next.mode = mode;
return { state: next, decision: { mode, reasons, unknown, critical: [...next.critical], releaseAt, notes } };
};
if (criticalNow.length) {
for (const c of criticalNow) if (!next.critical.includes(c)) next.critical.push(c);
next.releaseSince = null;
return decide("critical", [...next.critical]);
}
// Critical ends only on the caller's reconciliation, never on thresholds alone.
if (state.mode === "critical" && !reconciled) {
next.releaseSince = null;
return decide("critical", [...next.critical]);
}
const mode = state.mode === "critical" ? "held" : state.mode;
next.critical = [];
const reasons = [];
if (unknown.length) reasons.push("health-unknown");
if (loadHold) reasons.push("load-high");
if (memoryLow) reasons.push("memory-low");
if (psiHold) reasons.push(...psiOver);
if (reasons.length) { next.releaseSince = null; return decide("held", reasons); }
if (mode === "open") { next.releaseSince = null; return decide("open", []); }
if (!(loadReleases && memoryReleases && psiReleases)) { next.releaseSince = null; return decide("held", ["hysteresis"]); }
next.releaseSince = state.releaseSince !== null && contiguous ? state.releaseSince : now;
if (now - next.releaseSince >= rules.releaseMs) { next.releaseSince = null; return decide("open", []); }
return decide("held", ["release-pending"], next.releaseSince + rules.releaseMs);
}
+133
View File
@@ -0,0 +1,133 @@
// Parser and effective-limit tests. Every input is fixture text shaped like
// the kernel's files; nothing reads /proc, /sys or a cgroup.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
SupervisorError, parseLoadavg, parseMeminfo, parsePsi, parseCpuList, parseCpuMax, parseMemoryMax,
parseMemoryCurrent, parseMemoryEvents, parseVmstat, effectiveCpus, effectiveMemory,
} from "../src/index.mjs";
const GiB = 1024 ** 3;
const isParse = (re) => (e) => e instanceof SupervisorError && e.kind === "parse" && re.test(e.message);
const MEMINFO = [
"MemTotal: 32768000 kB",
"MemFree: 1024000 kB",
"MemAvailable: 8192000 kB",
"SwapTotal: 4096000 kB",
"SwapFree: 4000000 kB",
"HugePages_Total: 0",
"Hugepagesize: 2048 kB",
"",
].join("\n");
test("loadavg parses the three loads", () => {
assert.deepEqual(parseLoadavg("0.52 1.58 12.00 3/1234 56789\n"), { load1: 0.52, load5: 1.58, load15: 12 });
});
test("a malformed loadavg throws", () => {
for (const bad of ["", "0.52 0.58 0.59\n", "0.52 0.58 0.59 1/2\n", "x 0.58 0.59 1/2 3\n", "0.52 0.58 0.59 1/2 3 4\n", 42]) {
assert.throws(() => parseLoadavg(bad), isParse(/loadavg/), JSON.stringify(bad));
}
});
test("meminfo returns bytes and keeps unitless counts", () => {
assert.deepEqual(parseMeminfo(MEMINFO), {
memTotal: 32768000 * 1024, memAvailable: 8192000 * 1024, swapTotal: 4096000 * 1024, swapFree: 4000000 * 1024,
});
});
test("meminfo without MemAvailable, with a repeated key or a bad line throws", () => {
assert.throws(() => parseMeminfo("MemTotal: 1 kB\n"), isParse(/no MemAvailable/));
assert.throws(() => parseMeminfo(MEMINFO + "MemTotal: 1 kB\n"), isParse(/repeated key MemTotal/));
assert.throws(() => parseMeminfo(MEMINFO + "Broken line\n"), isParse(/bad line/));
assert.throws(() => parseMeminfo(MEMINFO.replace("8192000", "-1")), isParse(/bad line/));
});
test("PSI parses some and full, and cpu may lack full", () => {
const mem = "some avg10=1.50 avg60=0.20 avg300=0.00 total=123\nfull avg10=0.75 avg60=0.10 avg300=0.00 total=45\n";
assert.deepEqual(parsePsi(mem, "memory"), {
some: { avg10: 1.5, avg60: 0.2, avg300: 0, total: 123 },
full: { avg10: 0.75, avg60: 0.1, avg300: 0, total: 45 },
});
assert.equal(parsePsi("some avg10=20.00 avg60=0.00 avg300=0.00 total=9\n", "cpu").full, null);
});
test("PSI without a full line for memory or io, or with a bad line, throws", () => {
const someOnly = "some avg10=0.00 avg60=0.00 avg300=0.00 total=0\n";
assert.throws(() => parsePsi(someOnly, "memory"), isParse(/no full line/));
assert.throws(() => parsePsi(someOnly, "io"), isParse(/no full line/));
assert.throws(() => parsePsi("full avg10=0.00 avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/no some line/));
assert.throws(() => parsePsi(someOnly + someOnly, "cpu"), isParse(/repeated some/));
assert.throws(() => parsePsi("some avg10=x avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/not a non-negative number/));
assert.throws(() => parsePsi(someOnly, "gpu"), isParse(/unknown resource/));
});
test("CPU lists count distinct CPUs", () => {
assert.equal(parseCpuList("0-3,6,8-9\n"), 7);
assert.equal(parseCpuList("0\n"), 1);
assert.equal(parseCpuList("0-3,2-5"), 6);
for (const bad of ["", "\n", "3-1", "0,,1", "a-b", "0-"]) assert.throws(() => parseCpuList(bad), isParse(/cpu list/), bad);
});
test("cpu.max gives the quota in CPUs, or null for max", () => {
assert.equal(parseCpuMax("max 100000\n"), null);
assert.equal(parseCpuMax("50000 100000\n"), 0.5);
assert.equal(parseCpuMax("200000 100000\n"), 2);
for (const bad of ["max", "50000 0", "-1 100000", "max max"]) assert.throws(() => parseCpuMax(bad), isParse(/cpu.max/), bad);
});
test("memory.max, memory.current and memory.events", () => {
assert.equal(parseMemoryMax("max\n"), null);
assert.equal(parseMemoryMax("4294967296\n"), 4 * GiB);
assert.throws(() => parseMemoryMax("4G\n"), isParse(/memory.max/));
assert.equal(parseMemoryCurrent("123456\n"), 123456);
assert.throws(() => parseMemoryCurrent(""), isParse(/memory.current/));
assert.deepEqual(parseMemoryEvents("low 0\nhigh 4\nmax 2\noom 1\noom_kill 1\noom_group_kill 0\n"), { oom: 1, oomKill: 1 });
assert.throws(() => parseMemoryEvents("low 0\noom 1\n"), isParse(/no oom_kill/));
assert.throws(() => parseMemoryEvents("oom_kill 1\noom_kill 2\n"), isParse(/repeated key/));
});
test("vmstat gives swap counters and the host OOM counter", () => {
assert.deepEqual(parseVmstat("nr_free_pages 100\npswpin 7\npswpout 9\noom_kill 3\n"), { pswpin: 7, pswpout: 9, oomKill: 3 });
assert.deepEqual(parseVmstat("pswpin 0\npswpout 0\n"), { pswpin: 0, pswpout: 0, oomKill: null });
assert.throws(() => parseVmstat("pswpin 0\n"), isParse(/no pswpout/));
});
test("effective CPUs fall back to the host count with no cgroup input", () => {
assert.equal(effectiveCpus({ online: "0-15\n" }), 16);
});
test("effective CPUs take the smallest of host, cpuset and every quota level", () => {
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n" }), 4);
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n", cpuMax: ["max 100000\n", "150000 100000\n"] }), 1.5);
// The outer level is the tighter one.
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["800000 100000\n", "200000 100000\n"] }), 2);
// A quota above the host count doesn't raise it.
assert.equal(effectiveCpus({ online: "0-1\n", cpuMax: ["800000 100000\n"] }), 2);
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["max 100000\n"] }), 16);
});
test("effective memory falls back to the host with no cgroup levels, or only max", () => {
const host = { limit: 32768000 * 1024, available: 8192000 * 1024 };
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO }), host);
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "max\n" }] }), host);
});
test("effective memory uses the tightest limited level", () => {
// 4 GiB limit with 3 GiB used: 1 GiB available, below the host's MemAvailable.
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n`, current: `${3 * GiB}\n` }] }),
{ limit: 4 * GiB, available: 1 * GiB });
// Nested: the inner level is loose, the outer one is nearly full.
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [
{ max: "max\n" }, { max: `${16 * GiB}\n`, current: `${2 * GiB}\n` }, { max: `${6 * GiB}\n`, current: `${5.5 * GiB}\n` },
] }), { limit: 6 * GiB, available: 0.5 * GiB });
// Usage above the limit floors at 0.
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "100\n", current: "200\n" }] }).available, 0);
// A limit above MemTotal doesn't raise the limit.
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${64 * GiB}\n`, current: "0\n" }] }).limit, 32768000 * 1024);
});
test("a limited level without memory.current throws", () => {
assert.throws(() => effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n` }] }), isParse(/missing for a limited level/));
});
+28
View File
@@ -0,0 +1,28 @@
// S1 has no live enforcement: the source must not reach the filesystem, a
// process, the network or the clock. This reads the package's own source text.
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const SRC = path.join(import.meta.dirname, "..", "src");
const FORBIDDEN = [
/\bfrom\s+["'](node:)?(fs|fs\/promises|child_process|net|http|https|dgram|tls|worker_threads|os)["']/,
/\bimport\s*\(/,
/\brequire\s*\(/,
/\bprocess\./,
/\bDate\.now\b/,
/\bnew Date\b/,
/\bperformance\.now\b/,
/\bset(Timeout|Interval|Immediate)\b/,
];
test("src reads no file, process, network or clock", () => {
const files = fs.readdirSync(SRC).filter((f) => f.endsWith(".mjs"));
assert.deepEqual(files.sort(), ["errors.mjs", "index.mjs", "pressure.mjs", "rules.mjs"]);
for (const f of files) {
const body = fs.readFileSync(path.join(SRC, f), "utf8");
for (const re of FORBIDDEN) assert.doesNotMatch(body, re, `${f} matches ${re}`);
for (const m of body.matchAll(/\bfrom\s+["']([^"']+)["']/g)) assert.match(m[1], /^\.\/[a-z]+\.mjs$/, `${f} imports ${m[1]}`);
}
});
+339
View File
@@ -0,0 +1,339 @@
// Evaluator tests on a fake clock: each sample's `at` is the time. Synthetic
// numbers only; nothing samples the host or generates load.
import { test } from "node:test";
import assert from "node:assert/strict";
import { SupervisorError, DEFAULT_RULES, resolveRules, initialState, evaluate } from "../src/index.mjs";
const GiB = 1024 ** 3;
const LIMIT = 32 * GiB;
const R = DEFAULT_RULES;
// A healthy sample at `at`. `patch` changes fields per signal; null drops one.
function sample(at, patch = {}) {
const base = {
load: { load1: 0.1, cpus: 1 },
memory: { available: 16 * GiB, limit: LIMIT },
psi: { cpuSome: 0, memoryFull: 0, ioFull: 0 },
oom: { kills: 0 },
disk: { free: 100 * GiB },
};
const out = { at };
for (const [name, fields] of Object.entries(base)) {
if (patch[name] === null) continue;
out[name] = { at, ...fields, ...patch[name] };
}
return out;
}
// Fold samples through evaluate; returns every decision and the last state.
function run(state, samples, rules = R, opts) {
const decisions = [];
for (const s of samples) {
const r = evaluate(state, s, rules, typeof opts === "function" ? opts(s) : opts);
state = r.state;
decisions.push(r.decision);
}
return { state, decisions, last: decisions.at(-1) };
}
// Samples every 2 s from `from` to `to` inclusive.
function every2s(from, to, patch) {
const out = [];
for (let at = from; at <= to; at += 2000) out.push(sample(at, typeof patch === "function" ? patch(at) : patch));
return out;
}
// An open state whose last sample was at 30000.
function opened() {
const { state, last } = run(initialState(), every2s(0, 30000));
assert.equal(last.mode, "open");
return state;
}
// A state held by a load hold, last sample at 36000.
function heldByLoad() {
const { state, last } = run(opened(), every2s(32000, 36000, { load: { load1: 0.9 } }));
assert.deepEqual(last.reasons, ["load-high"]);
return state;
}
const modes = (r) => r.decisions.map((d) => d.mode);
test("the first state is held, and healthy samples open it after exactly 30 s", () => {
assert.equal(initialState().mode, "held");
const first = evaluate(initialState(), sample(0));
assert.equal(first.decision.mode, "held");
assert.deepEqual(first.decision.reasons, ["release-pending"]);
assert.equal(first.decision.releaseAt, 30000);
const r = run(initialState(), [sample(0), sample(2000), sample(6000), sample(10000), sample(14000), sample(18000),
sample(22000), sample(26000), sample(29999), sample(30000)]);
assert.deepEqual(r.decisions.at(-2).reasons, ["release-pending"]);
assert.equal(r.decisions.at(-2).mode, "held");
assert.equal(r.last.mode, "open");
});
test("load holds at 0.80 for 3 samples and not at 0.7999", () => {
const at = run(opened(), every2s(32000, 36000, { load: { load1: 0.8 } }));
assert.deepEqual(modes(at), ["open", "open", "held"]);
assert.deepEqual(at.last.reasons, ["load-high"]);
const below = run(opened(), every2s(32000, 50000, { load: { load1: 0.7999 } }));
assert.ok(below.decisions.every((d) => d.mode === "open"));
});
test("load divides by effective CPUs, fractional ones included", () => {
const r = run(opened(), every2s(32000, 36000, { load: { load1: 0.4, cpus: 0.5 } }));
assert.equal(r.last.mode, "held");
const r2 = run(opened(), every2s(32000, 36000, { load: { load1: 3.1, cpus: 4 } }));
assert.equal(r2.last.mode, "open");
});
test("a gap of more than 6 s resets the consecutive count", () => {
const high = { load: { load1: 0.9 } };
const r = run(opened(), [sample(32000, high), sample(34000, high), sample(40001, high), sample(42001, high), sample(44001, high)]);
assert.deepEqual(modes(r), ["open", "open", "open", "open", "held"]);
const ok = run(opened(), [sample(32000, high), sample(34000, high), sample(40000, high)]);
assert.equal(ok.last.mode, "held");
});
test("load releases at 0.55 and not at 0.5501", () => {
const dead = run(heldByLoad(), every2s(38000, 80000, { load: { load1: 0.5501 } }));
assert.ok(dead.decisions.every((d) => d.mode === "held" && d.reasons[0] === "hysteresis"));
const r = run(heldByLoad(), every2s(38000, 68000, { load: { load1: 0.55 } }));
assert.equal(r.decisions.at(-2).mode, "held");
assert.equal(r.last.mode, "open");
});
test("a dead-band sample restarts the 30 s release window", () => {
const r = run(heldByLoad(), [
...every2s(38000, 60000),
sample(62000, { load: { load1: 0.6 } }),
...every2s(64000, 92000),
sample(94000),
]);
const at = (t) => r.decisions[r.decisions.length - 1 - (94000 - t) / 2000];
assert.equal(at(62000).reasons[0], "hysteresis");
assert.equal(at(92000).mode, "held");
assert.equal(at(94000).mode, "open");
});
test("memory holds below the larger floor, on one sample", () => {
const floor = Math.max(R.memory.holdFraction * LIMIT, R.memory.holdBytes);
assert.ok(floor > R.memory.holdBytes); // 15% of 32 GiB is the larger floor here
const just = Math.ceil(floor);
assert.equal(evaluate(opened(), sample(32000, { memory: { available: just } })).decision.mode, "open");
const low = evaluate(opened(), sample(32000, { memory: { available: just - 1 } })).decision;
assert.equal(low.mode, "held");
assert.deepEqual(low.reasons, ["memory-low"]);
});
test("on a small limit the 1 GiB floor is the larger one", () => {
const small = 4 * GiB; // 15% is 0.6 GiB, so 1 GiB applies
const s = (available) => sample(32000, { memory: { available, limit: small } });
assert.equal(evaluate(opened(), s(1 * GiB)).decision.mode, "open");
assert.deepEqual(evaluate(opened(), s(1 * GiB - 1)).decision.reasons, ["memory-low"]);
});
test("memory releases only above the larger release floor", () => {
// 3 GiB is below the 15% floor (4.8 GiB) and above the 5% critical line (1.6 GiB).
const held = evaluate(opened(), sample(32000, { memory: { available: 3 * GiB } })).state;
assert.equal(held.mode, "held");
const rel = Math.max(R.memory.releaseFraction * LIMIT, R.memory.releaseBytes);
const stay = run(held, every2s(34000, 80000, { memory: { available: Math.floor(rel) } }));
assert.ok(stay.decisions.every((d) => d.mode === "held"));
const go = run(held, every2s(34000, 64000, { memory: { available: Math.floor(rel) + 1 } }));
assert.equal(go.last.mode, "open");
const small = { limit: 4 * GiB }; // 25% is 1 GiB, so 2 GiB applies
const stay2 = run(held, every2s(34000, 80000, { memory: { ...small, available: 2 * GiB } }));
assert.ok(stay2.decisions.every((d) => d.mode === "held"));
const go2 = run(held, every2s(34000, 64000, { memory: { ...small, available: 2 * GiB + 1 } }));
assert.equal(go2.last.mode, "open");
});
test("each PSI hold threshold holds at its value for 3 samples and not just below", () => {
for (const [field, code] of [["cpuSome", "psi-cpu"], ["memoryFull", "psi-memory"], ["ioFull", "psi-io"]]) {
const at = run(opened(), every2s(32000, 36000, { psi: { [field]: R.psi.hold[field] } }));
assert.deepEqual(modes(at), ["open", "open", "held"], field);
assert.deepEqual(at.last.reasons, [code], field);
const below = run(opened(), every2s(32000, 50000, { psi: { [field]: R.psi.hold[field] - 0.01 } }));
assert.ok(below.decisions.every((d) => d.mode === "open"), field);
}
});
test("the PSI count runs across resources", () => {
const r = run(opened(), [sample(32000, { psi: { cpuSome: 25 } }), sample(34000, { psi: { memoryFull: 3 } }), sample(36000, { psi: { ioFull: 12 } })]);
assert.deepEqual(modes(r), ["open", "open", "held"]);
assert.deepEqual(r.last.reasons, ["psi-io"]);
});
test("each PSI release threshold blocks at its value and releases just below", () => {
const held = run(opened(), every2s(32000, 36000, { psi: { cpuSome: 30 } })).state;
for (const field of ["cpuSome", "memoryFull", "ioFull"]) {
const stay = run(held, every2s(38000, 80000, { psi: { [field]: R.psi.release[field] } }));
assert.ok(stay.decisions.every((d) => d.mode === "held" && d.reasons[0] === "hysteresis"), field);
const go = run(held, every2s(38000, 68000, { psi: { [field]: R.psi.release[field] - 0.01 } }));
assert.equal(go.last.mode, "open", field);
}
});
test("a missing required signal holds at once with health-unknown", () => {
for (const name of ["load", "memory", "oom", "disk"]) {
const d = evaluate(opened(), sample(32000, { [name]: null })).decision;
assert.equal(d.mode, "held", name);
assert.deepEqual(d.reasons, ["health-unknown"], name);
assert.deepEqual(d.unknown, [name], name);
}
});
test("a signal is stale at more than 6 s old and fresh at exactly 6 s", () => {
const stale = { at: 32000, load1: 0.1, cpus: 1 };
const s = (signalAt) => ({ ...sample(32000), load: { ...stale, at: signalAt } });
assert.equal(evaluate(opened(), s(26000)).decision.mode, "open");
assert.deepEqual(evaluate(opened(), s(25999)).decision.unknown, ["load"]);
assert.deepEqual(evaluate(opened(), s(32001)).decision.unknown, ["load"]); // later than the sample
});
test("malformed signal values count as missing", () => {
for (const patch of [{ load: { load1: NaN } }, { load: { cpus: 0 } }, { load: { load1: -1 } }, { memory: { limit: 0 } },
{ memory: { available: "1" } }, { oom: { kills: Infinity } }, { disk: { free: undefined } }]) {
const d = evaluate(opened(), sample(32000, patch)).decision;
assert.deepEqual(d.reasons, ["health-unknown"], JSON.stringify(patch));
}
});
test("after health-unknown, recovery waits the full 30 s", () => {
const unknown = evaluate(opened(), sample(32000, { load: null })).state;
const r = run(unknown, every2s(34000, 64000));
assert.equal(r.decisions.at(-2).mode, "held");
assert.equal(r.last.mode, "open");
});
test("a host that never had PSI skips the PSI rows", () => {
const r = run(initialState(), every2s(0, 30000, { psi: null }));
assert.equal(r.last.mode, "open");
assert.deepEqual(r.last.unknown, []);
});
test("PSI that disappears after it was present holds with health-unknown", () => {
const d = evaluate(opened(), sample(32000, { psi: null })).decision;
assert.equal(d.mode, "held");
assert.deepEqual(d.reasons, ["health-unknown"]);
assert.deepEqual(d.unknown, ["psi"]);
// Also when it first appears late, then goes.
const late = run(initialState(), [...every2s(0, 30000, { psi: null }), sample(32000), sample(34000, { psi: null })]);
assert.deepEqual(late.last.unknown, ["psi"]);
// A stale PSI signal counts the same.
const stalePsi = { ...sample(32000), psi: { at: 25000, cpuSome: 0, memoryFull: 0, ioFull: 0 } };
assert.deepEqual(evaluate(opened(), stalePsi).decision.unknown, ["psi"]);
});
test("a rise in the OOM kill counter is critical; the first sample is the baseline", () => {
const first = evaluate(initialState(), sample(0, { oom: { kills: 5 } }));
assert.notEqual(first.decision.mode, "critical");
const r = run(opened(), [sample(32000, { oom: { kills: 0 } }), sample(34000, { oom: { kills: 1 } })]);
assert.equal(r.last.mode, "critical");
assert.deepEqual(r.last.reasons, ["critical-oom"]);
});
test("a falling OOM counter resets the baseline with a note, not an event", () => {
const s = run(initialState(), every2s(0, 30000, { oom: { kills: 6 } })).state;
assert.equal(s.mode, "open");
const r = run(s, [sample(32000, { oom: { kills: 2 } }), sample(34000, { oom: { kills: 2 } })]);
assert.deepEqual(r.decisions[0].notes, ["oom-counter-reset"]);
assert.ok(r.decisions.every((d) => d.mode === "open"));
});
test("memory under 5% and disk under 1 GiB are critical, at their edges", () => {
const five = R.critical.memoryFraction * LIMIT;
assert.equal(evaluate(opened(), sample(32000, { memory: { available: Math.ceil(five) } })).decision.mode, "held");
const mem = evaluate(opened(), sample(32000, { memory: { available: Math.ceil(five) - 1 } })).decision;
assert.equal(mem.mode, "critical");
assert.deepEqual(mem.reasons, ["critical-memory"]);
// On 20 GiB the 5% line is exactly 1 GiB, so the edge itself is tested.
const s20 = (available) => sample(32000, { memory: { available, limit: 20 * GiB } });
assert.equal(evaluate(opened(), s20(1 * GiB)).decision.mode, "held");
assert.deepEqual(evaluate(opened(), s20(1 * GiB - 1)).decision.reasons, ["critical-memory"]);
assert.equal(evaluate(opened(), sample(32000, { disk: { free: 1 * GiB } })).decision.mode, "open");
assert.deepEqual(evaluate(opened(), sample(32000, { disk: { free: 1 * GiB - 1 } })).decision.reasons, ["critical-disk"]);
});
test("critical stays until reconciliation, then still waits the 30 s release", () => {
const crit = evaluate(opened(), sample(32000, { disk: { free: 0 } })).state;
const waiting = run(crit, every2s(34000, 120000));
assert.ok(waiting.decisions.every((d) => d.mode === "critical" && d.reasons[0] === "critical-disk"));
// Reconciled while the condition is still there: still critical.
assert.equal(evaluate(crit, sample(34000, { disk: { free: 0 } }), R, { reconciled: true }).decision.mode, "critical");
// Reconciled and healthy: held, then open 30 s later.
const after = run(waiting.state, every2s(122000, 152000), R, (s) => ({ reconciled: s.at === 122000 }));
assert.deepEqual(after.decisions[0].reasons, ["release-pending"]);
assert.deepEqual(after.decisions[0].critical, []);
assert.equal(after.decisions.at(-2).mode, "held");
assert.equal(after.last.mode, "open");
});
test("critical reasons accumulate while critical", () => {
const r = run(opened(), [sample(32000, { disk: { free: 0 } }), sample(34000, { oom: { kills: 3 } })]);
assert.deepEqual(r.last.reasons, ["critical-disk", "critical-oom"]);
});
test("a clock that goes backwards holds with health-unknown", () => {
const d = evaluate(opened(), sample(20000)).decision;
assert.equal(d.mode, "held");
assert.deepEqual(d.unknown, ["clock"]);
assert.deepEqual(d.notes, ["clock-backwards"]);
});
test("a sample without a numeric time throws", () => {
for (const bad of [null, {}, { at: "1" }, { at: NaN }]) {
assert.throws(() => evaluate(initialState(), bad), (e) => e instanceof SupervisorError && e.kind === "sample");
}
});
test("evaluate doesn't change the state it was given", () => {
const s = Object.freeze({ ...opened(), critical: Object.freeze([]) });
assert.doesNotThrow(() => evaluate(s, sample(32000, { disk: { free: 0 } })));
assert.equal(s.mode, "open");
});
test("resolveRules with no overrides is the D4 table, frozen", () => {
assert.deepEqual(resolveRules(), DEFAULT_RULES);
assert.ok(Object.isFrozen(DEFAULT_RULES) && Object.isFrozen(DEFAULT_RULES.psi.hold));
assert.deepEqual(DEFAULT_RULES.load, { hold: 0.8, release: 0.55, samples: 3 });
assert.equal(DEFAULT_RULES.releaseMs, 30000);
assert.equal(DEFAULT_RULES.staleMs, 6000);
});
test("overrides apply in the evaluator", () => {
const rules = resolveRules({ releaseMs: 10000, load: { samples: 1 } });
const r = run(initialState(), every2s(0, 10000), rules);
assert.equal(r.last.mode, "open");
assert.equal(evaluate(r.state, sample(12000, { load: { load1: 0.9 } }), rules).decision.mode, "held");
});
test("contradictory or unknown settings refuse", () => {
const cases = [
[{ load: { release: 0.8 } }, /load.release must be above 0 and below load.hold/],
[{ load: { hold: 0.5 } }, /load.release must be above 0 and below load.hold/],
[{ memory: { releaseFraction: 0.15 } }, /releaseFraction must be above/],
[{ memory: { releaseBytes: 1 * GiB } }, /releaseBytes must be above/],
[{ memory: { holdFraction: 1.2 } }, /between 0 and 1/],
[{ psi: { release: { ioFull: 10 } } }, /psi.release.ioFull must be above 0 and below psi.hold.ioFull/],
[{ psi: { hold: { cpuSome: 120 } } }, /percentages/],
[{ psi: { samples: 0 } }, /psi.samples must be an integer/],
[{ load: { samples: 1.5 } }, /load.samples must be an integer/],
[{ staleMs: 2000 }, /staleMs must be above sampleMs/],
[{ releaseMs: -1 }, /releaseMs must not be negative/],
[{ critical: { memoryFraction: 0.15 } }, /critical.memoryFraction/],
[{ critical: { diskBytes: 0 } }, /critical.diskBytes/],
[{ loadd: {} }, /unknown key loadd/],
[{ psi: { hold: { gpu: 1 } } }, /unknown key psi.hold.gpu/],
[{ load: { toString: 1 } }, /unknown key load.toString/],
[{ constructor: {} }, /unknown key constructor/],
[JSON.parse('{"memory":{"__proto__":{}}}'), /unknown key memory.__proto__/],
[{ load: { hold: "0.8" } }, /load.hold must be a finite number/],
[{ load: 1 }, /load must be an object/],
[[], /rules must be an object/],
];
for (const [over, re] of cases) {
assert.throws(() => resolveRules(over), (e) => e instanceof SupervisorError && e.kind === "rules" && re.test(e.message), JSON.stringify(over));
}
});
+48 -3
View File
@@ -9,7 +9,14 @@ const json = (res, status, value) => {
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
res.end(JSON.stringify(value));
};
const ROOM_LINES = Object.freeze({
'unknown-addressee': 'Unknown addressee. Choose a room member.',
'addressee-ambiguous': "That addressee is ambiguous. Use a room member's instance name.",
'room-thread-name-taken': 'A room thread name is already in use. Resolve the conflicting thread before opening the room.',
'room-not-open': 'This room is not open. Open the room before sending.',
});
const STATUS = {
...Object.fromEntries(Object.keys(ROOM_LINES).map(code => [code, 409])),
'channel-disabled': 503,
'invalid-request': 400, 'unknown-repo': 400, 'message-not-found': 400,
'project-not-found': 404, 'thread-not-found': 404, 'thread-not-owned': 403,
@@ -26,6 +33,7 @@ const STATUS = {
'access-refused': 403, 'access-revoked': 401,
};
function safeMessage(code) {
if (Object.hasOwn(ROOM_LINES, code)) return ROOM_LINES[code];
if (code === 'unknown-verb' || code === 'channel-operation') return 'This session operation is unavailable until its dependency is installed.';
if (code === 'channel-disabled') return 'Run mosaic plugins to read the revision, then mosaic plugins set --file PATCH with plugins.channel set to ["cli","webui"].';
if (code === 'thread-not-owned') return 'This thread is not available to this user.';
@@ -53,7 +61,7 @@ const exact = (value, allowed, required = allowed) =>
Object.keys(value).every(k => allowed.includes(k)) && required.every(k => Object.hasOwn(value, k));
export async function chatHTTP(req, res, { path, search, chat, named, send, preview, log }) {
const write = req.method === 'POST';
let action, args, thread;
let action, args, thread, room;
const refuse = (status, code, sent = false) => json(res, status, {
error: code, message: safeMessage(code), ...(write ? { sent } : {}),
});
@@ -73,6 +81,12 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site');
}
try {
const roomMatch = /^\/api\/chat\/rooms\/([^/]+)\/(open|messages)$/.exec(path);
if (roomMatch) {
room = roomMatch[1];
if (!id(room)) throw Error();
if (preview?.tester) return refuse(403, 'human-required');
}
const match = /^\/api\/chat\/threads\/([^/]+)\/(messages|open|sessions|clear|cancel|operation|shares|share|unshare)$/.exec(path);
if (match) {
thread = match[1];
@@ -82,7 +96,15 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
if (write) {
query(search, []);
const v = await payload(req);
if (path === '/api/chat/projects') {
if (roomMatch) {
if (roomMatch[2] === 'open') {
if (!exact(v, [])) throw Error();
action = 'roomOpen'; args = { project: room };
} else {
if (!exact(v, ['body']) || typeof v.body !== 'string' || !v.body.trim() || v.body.length > 32768) throw Error();
action = 'roomSend'; args = { project: room, body: v.body };
}
} else if (path === '/api/chat/projects') {
if (!exact(v, ['name', 'repo'], ['name']) || !name(v.name) ||
(Object.hasOwn(v, 'repo') && (typeof v.repo !== 'string' || !KEY.test(v.repo)))) throw Error();
action = 'createProject'; args = v;
@@ -110,6 +132,12 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
if (!exact(v, ['session'], []) || (Object.hasOwn(v, 'session') && !id(v.session))) throw Error();
action = 'open'; args = { thread, ...(v.session ? { session: v.session } : {}) };
} else return refuse(404, 'not-found');
} else if (roomMatch) {
if (roomMatch[2] !== 'messages') return refuse(405, 'method-not-allowed');
const q = query(search, ['after', 'limit']);
if (q.has('after') && !id(q.get('after'))) throw Error();
if (q.has('limit') && (!/^[1-9]\d*$/.test(q.get('limit')) || Number(q.get('limit')) > 200)) throw Error();
action = 'roomRead'; args = { project: room, limit: q.has('limit') ? Number(q.get('limit')) : 100, ...(q.has('after') ? { after: q.get('after') } : {}) };
} else if (path === '/api/chat/projects') {
query(search, []); action = 'projects'; args = {};
} else if (path === '/api/chat/threads') {
@@ -139,7 +167,24 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
const line = outcome => log(`${new Date().toISOString()} chat action=${action} user=${JSON.stringify(preview?.user ?? null)} ${outcome}`);
try {
const value = await chat[action](args);
if (action === 'sessionOperation' || action === 'clearSession' || action === 'cancelSession') {
if (action === 'roomOpen' || action === 'roomSend') {
const valid = action === 'roomOpen'
? value?.project === room && Array.isArray(value.threads) && value.threads.length > 0 && value.threads.every(t => id(t?.id) && typeof t.instance === 'string' && KEY.test(t.instance) && t.name === t.instance)
&& new Set(value.threads.map(t => t.id)).size === value.threads.length && new Set(value.threads.map(t => t.instance)).size === value.threads.length
: Array.isArray(value?.ids) && value.ids.length > 0 && value.ids.every(id) && new Set(value.ids).size === value.ids.length
&& Array.isArray(value.to) && value.to.length === value.ids.length && value.to.every(k => typeof k === 'string' && KEY.test(k)) && new Set(value.to).size === value.to.length;
if (!valid) { line('error=outcome-unknown'); return refuse(503, 'outcome-unknown', 'unknown'); }
line(`project=${room}`);
return json(res, 200, action === 'roomOpen'
? { project: room, threads: value.threads.map(({ id, instance, name }) => ({ id, instance, name })) }
: { ids: value.ids, to: value.to });
}
if (action === 'roomRead') {
if (!Array.isArray(value) || value.some(m => !id(m?.id) || !Array.isArray(m.ids) || !m.ids.length || m.ids[0] !== m.id || !m.ids.every(id)
|| new Set(m.ids).size !== m.ids.length || !Array.isArray(m.to) || new Set(m.to).size !== m.to.length || m.to.length !== m.ids.length || !m.to.every(k => typeof k === 'string' && KEY.test(k)))) return refuse(502, 'invalid-response');
return json(res, 200, { rows: value, after: value.at(-1)?.ids.at(-1) ?? args.after ?? null, hasMore: value.length === args.limit, at: new Date().toISOString() });
}
if (action === 'sessionOperation' || action === 'clearSession' || action === 'cancelSession') {
// Only this operation's receipt crosses the HTTP seam. Never expose native paths/proofs.
if (!value || value.thread !== args.thread || value.op !== args.op || !id(value.session) ||
!id(value.successor) || (action === 'clearSession' && value.session !== args.session) ||
+3
View File
@@ -22,6 +22,9 @@ export function createChat({ call, owner = null }) {
}
};
return Object.freeze({
roomOpen: ({ project }) => channel('room.open', { project }),
roomRead: ({ project, after, limit }) => channel('room.read', { project, ...(after ? { after } : {}), ...(limit === undefined ? {} : { limit }) }),
roomSend: ({ project, body }) => channel('room.send', { project, body }),
projects: () => channel('project.list', {}),
createProject: ({ name, repo }) => channel('project.create', { name, ...(repo === undefined ? {} : { repo }) }),
threads: ({ project, shared } = {}) => channel('thread.list', { ...(project === undefined ? {} : { project }), ...(shared === true ? { shared: true } : {}) }),
+10
View File
@@ -40,3 +40,13 @@
.chat-library { font-size: .9rem; margin-block: .5rem; overflow-wrap: anywhere; }
.chat-library p { margin: 0; }
.chat-library-problems ul { margin: .4rem 0 0; padding-inline-start: 1.25rem; }
/* C2-10c: the shared timeline and keyboard-accessible leading-@ completion. */
.chat-room-entry { margin-block: .75rem; }
.chat-room #chat-room-send-form { display: grid; gap: .5rem; }
#chat-room-body { resize: vertical; min-height: 6rem; padding: .75rem; }
.chat-room-completion { list-style: none; padding: .25rem; margin: 0; max-height: 12rem; overflow: auto; border: 1px solid var(--border); border-radius: .5rem; }
.chat-room-completion button { display: block; width: 100%; text-align: start; padding: .5rem; overflow-wrap: anywhere; }
.chat-room-completion [aria-selected="true"] { outline: 2px solid var(--action); outline-offset: -2px; }
#chat-room-tags, #chat-room-receipt { overflow-wrap: anywhere; }
#chat-room-tags span { display: inline-block; margin-inline-end: .4rem; }
+171 -5
View File
@@ -24,7 +24,7 @@
// The library verbs name the business project, which is the chat project's repository link. These
// codes mean that repository is no library, or this page may not read it: no status line at all.
const NO_LIBRARY = ['project-not-declared', 'project-not-configured', 'not-configured', 'owner-only'];
const libraryOf = (c, t) => { const repo = c?.projects.find(p => p.id === t?.project)?.repo; return typeof repo === 'string' && NAME.test(repo) ? repo : null; };
const libraryOf = (c, t) => { const repo = c?.projects.find(p => p.id === (c.kind === 'room' ? c.id : t?.project))?.repo; return typeof repo === 'string' && NAME.test(repo) ? repo : null; };
let renderReply = null;
import('/render.mjs').then(m => { renderReply = m.renderReply; if (current && !out()) paint(); }, () => {});
const libraries = new Map(), reverts = new Map();
@@ -191,6 +191,162 @@
const receipt = s.receipt;
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${libraryLine(libraryOf(current, t))}${sessionControls(t)}${sharing(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p>${threadWrites() ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button>` : ''}<ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div>${threadWrites() ? `<form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form>` : ''}<p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
}
// C2-10c: rooms reuse the same state, renderer and generation fences as threads.
const ROOM_LINES = Object.freeze({
'unknown-addressee': 'Unknown addressee. Choose a room member.',
'addressee-ambiguous': "That addressee is ambiguous. Use a room member's instance name.",
'room-thread-name-taken': 'A room thread name is already in use. Resolve the conflicting thread before opening the room.',
'room-not-open': 'This room is not open. Open the room before sending.',
});
const roomError = err => ROOM_LINES[err.code] ?? (err.sent === false || err.status >= 400 && err.status < 500
? `Room operation refused (${inert(err.code)}).`
: 'Room unavailable. Refresh to read it again; nothing was retried.');
const roomOwner = () => current && !current.user && !current.tester;
// Same leading-token grammar as bus/address.mjs; the UI never resolves/replaces recipients on send.
function roomAddress(input) {
const tags = []; let body = input.trimStart();
while (true) {
const token = /^@(\S+)(?:\s+|$)/u.exec(body);
if (!token) break;
tags.push(token[1]); body = body.slice(token[0].length);
}
return { tags, body };
}
const roomName = instance => current.members?.find(m => m.instance === instance)?.name || instance;
function roomOpenControl(project) {
if (!roomOwner()) return '';
const f = form('room-open:' + project);
return `<section class="chat-room-entry" aria-label="Project room"><button class="btn" type="button" id="chat-room-open"${disabled(f.pending)}>Open room</button> <a href="#/chat/rooms/${esc(project)}">Read room</a><p id="chat-room-open-receipt" role="status">${txt(f.receipt)}</p></section>`;
}
function roomConversation() {
const c = current, s = state(c.key), p = c.projects.find(p => p.id === c.id);
const messages = s.rows.map(m => {
const speaker = m.from_role === 'human' ? 'Human' : roomName(m.from_role);
const recipients = m.from_role === 'human' ? ` → ${(m.to ?? []).map(r => txt(roomName(r))).join(', ')}` : '';
return `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(speaker)}</strong>${recipients} ${stamp(m.at)}</div>${messageBody({ ...m, from_role: m.from_role === 'human' ? m.from_role : speaker })}</li>`;
}).join('');
const off = !c.roomReady || s.pending || !roomOwner();
return `<section class="chat-room" data-project="${esc(c.id)}"><p>${link('projects', c.id, p?.name ?? 'Project')}</p>${heading((p?.name ?? 'Project') + ' room')}${roomOpenControl(c.id)}${libraryLine(libraryOf(c))}<p>Room members: ${(c.members ?? []).map(m => `${txt(m.name)} (@${txt(m.instance)})`).join(', ') || 'unavailable'}</p><ol class="chat-log chat-room-log" tabindex="0" aria-label="Room conversation">${messages || '<li>No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div><form id="chat-room-send-form"><label for="chat-room-body">Message the room</label><p id="chat-room-help">Without leading @tags, your message goes to every member. The broker checks the addressees.</p><textarea id="chat-room-body" rows="4" aria-describedby="chat-room-help" aria-autocomplete="list" aria-controls="chat-room-completion" aria-expanded="false"${disabled(off)}>${esc(s.draft)}</textarea><div id="chat-room-tags"></div><ul id="chat-room-completion" class="chat-room-completion" role="listbox" aria-label="Room members" hidden></ul><button class="btn primary" type="submit"${disabled(off)}>Send to room</button></form><p id="chat-room-receipt" role="status">${txt(s.receipt)}</p></section>`;
}
function drawRoomCompletion() {
const list = document.getElementById('chat-room-completion'), field = document.getElementById('chat-room-body');
if (!list || !field) return;
const c = current.completion, active = !!c?.items.length;
list.hidden = !active; field.setAttribute('aria-expanded', String(active));
if (!active) { list.replaceChildren(); field.removeAttribute('aria-activedescendant'); return; }
list.innerHTML = c.items.map((m, i) => `<li role="presentation"><button type="button" role="option" tabindex="-1" id="chat-room-option-${i}" aria-selected="${i === c.index}" data-room-complete="${esc(m.instance)}">${txt(m.name)} (@${txt(m.instance)})</button></li>`).join('');
field.setAttribute('aria-activedescendant', `chat-room-option-${c.index}`);
}
function updateRoomCompletion() {
if (current?.kind !== 'room') return;
const field = document.getElementById('chat-room-body'), preview = document.getElementById('chat-room-tags');
if (!field || !preview) return;
const { tags } = roomAddress(field.value);
preview.innerHTML = tags.length ? 'Typed addressees: ' + tags.map(tag => `<span data-room-tag="${esc(tag)}">@${txt(tag)}</span>`).join(' ') : 'All room members';
const prefix = field.value.slice(0, field.selectionStart), token = /(?:^|\s)@([^\s@]*)$/u.exec(prefix);
current.completion = null;
if (document.activeElement === field && !field.disabled && token && !roomAddress(prefix.slice(0, token.index)).body.trim()) {
const needle = token[1].toLocaleLowerCase();
const items = (current.members ?? []).filter(m => m.instance.toLocaleLowerCase().startsWith(needle) || m.name.toLocaleLowerCase().startsWith(needle)).slice(0, 10);
current.completion = { items, index: 0, start: prefix.length - token[1].length - 1, end: field.selectionEnd };
}
drawRoomCompletion();
}
function completeRoom(instance) {
const field = document.getElementById('chat-room-body'), c = current?.completion;
if (!field || field.disabled || !c?.items.some(m => m.instance === instance)) return;
const value = field.value.slice(0, c.start) + '@' + instance + ' ' + field.value.slice(c.end);
const caret = c.start + instance.length + 2;
field.value = value; state(current.key).draft = value; field.focus(); field.setSelectionRange(caret, caret);
updateRoomCompletion();
}
async function renderRoom({ path, container, canSend, user, tester, navigated, next }) {
if (out()) return;
controller?.abort(); controller = new AbortController();
const signal = controller.signal, g = ++generation;
const match = /^\/chat\/rooms\/([0-9a-f-]+)$/i.exec(path), id = match?.[1];
current = { kind: 'room', id, key: 'room:' + id, path, container, canSend, user, tester,
projects: [], threads: [], members: [], roomReady: false, refused: false, error: '' };
if (!UUID.test(id ?? '') || !roomOwner()) {
current.error = UUID.test(id ?? '') ? 'Rooms require the business owner.' : 'Invalid room route.';
current.refused = true; paint(navigated); return;
}
const s = state(current.key);
if (navigated) container.innerHTML = '<p role="status">Reading room…</p>';
try {
const [projects, threads, catalog] = await Promise.all([
request('projects', undefined, signal), request('threads?project=' + id, undefined, signal), request('instances', undefined, signal),
]);
if (g !== generation || out()) return;
if (!projects.rows.some(p => p.id === id)) throw new ChatError('project-not-found', 404);
if (!catalog.rows.every(m => typeof m?.instance === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$/.test(m.instance) && typeof m.name === 'string')) throw new ChatError('invalid-response');
current.projects = projects.rows; current.threads = threads.rows;
current.members = catalog.rows.filter(m => threads.rows.some(t => t.project === id && t.instance === m.instance && t.name === m.instance));
const after = s.loaded && (!s.more || next) ? s.after : null;
const page = await request(`rooms/${id}/messages?limit=100${after ? '&after=' + after : ''}`, undefined, signal);
if (g !== generation || out()) return;
if (typeof page.hasMore !== 'boolean' || !(page.after === null || UUID.test(page.after)) || page.rows.some(m => !UUID.test(m?.id) || !Array.isArray(m.ids) || !m.ids.every(id => UUID.test(id)) || !Array.isArray(m.to))) throw new ChatError('invalid-response');
s.rows = [...new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.id, m])).values()].slice(-200);
s.after = page.after ?? s.after; s.more = page.hasMore; s.loaded = true;
current.roomReady = current.members.length > 0;
if (s.receiptIds?.every(id => s.rows.some(m => m.ids.includes(id)))) { s.receipt = 'Message shown in the room.'; s.receiptIds = null; }
} catch (err) {
if (g !== generation || out() || err.name === 'AbortError' || err.code === 'signed-out') return;
current.error = roomError(err);
if (err.status === 403 || err.status === 404) { s.rows = []; current.members = []; }
}
if (g === generation && !out()) { paint(navigated); void loadLibrary(libraryOf(current), g, signal); }
}
async function openRoom() {
const c = current, id = c?.id;
if (!roomOwner() || !c.canSend || out() || !UUID.test(id ?? '')) return;
const f = form('room-open:' + id);
if (f.pending) return;
f.pending = true; f.receipt = ''; paint();
try {
const data = await request(`rooms/${id}/open`, {});
if (out()) return;
if (data.project !== id || !Array.isArray(data.threads) || !data.threads.length || data.threads.some(t => !UUID.test(t?.id) || typeof t.instance !== 'string' || t.name !== t.instance)) throw new ChatError('outcome-unknown');
f.receipt = 'Room opened.';
if (current?.id === id) {
if (location.hash === '#/chat/rooms/' + id) void refresh();
else location.hash = '#/chat/rooms/' + id;
}
} catch (err) {
if (out() || err.code === 'signed-out') return;
f.receipt = err.sent === false ? roomError(err) : 'Outcome unknown. Read the room before opening again; nothing was retried.';
} finally { f.pending = false; if (current?.id === id && !out()) paint(); }
}
async function sendRoom() {
const c = current;
if (!roomOwner() || !c.canSend || !c.roomReady || out()) return;
const s = state(c.key), body = s.draft;
if (s.pending) return;
if (!body.trim() || new TextEncoder().encode(JSON.stringify({ body })).length > 16384) {
s.receipt = body.trim() ? 'Message is too long (16 KiB JSON maximum).' : 'Write a message first.'; paint(); return;
}
s.pending = true; s.receipt = ''; paint();
try {
const data = await request(`rooms/${c.id}/messages`, { body });
if (out()) return;
if (!Array.isArray(data.ids) || !data.ids.length || !data.ids.every(id => UUID.test(id)) || !Array.isArray(data.to) || data.to.length !== data.ids.length) throw new ChatError('outcome-unknown');
s.receipt = 'Accepted room message. Waiting for the room to show it.'; s.receiptIds = data.ids;
if (s.draft === body) s.draft = '';
s.follow = true; s.tailRequested = true;
} catch (err) {
if (out() || err.code === 'signed-out') return;
s.receipt = err.sent === false ? roomError(err) : 'Outcome unknown. Read the room before trying again. Nothing was retried; your draft is kept.';
} finally { s.pending = false; if (current?.key === c.key && !out()) paint(); }
}
document.addEventListener('keydown', event => {
if (current?.kind !== 'room' || event.target.id !== 'chat-room-body' || !current.completion?.items.length) return;
const c = current.completion;
if (['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); c.index = (c.index + (event.key === 'ArrowDown' ? 1 : c.items.length - 1)) % c.items.length; drawRoomCompletion(); }
if (['Enter', 'Tab'].includes(event.key)) { event.preventDefault(); completeRoom(c.items[c.index].instance); }
if (event.key === 'Escape') { event.preventDefault(); current.completion = null; drawRoomCompletion(); }
});
document.addEventListener('mousedown', event => { if (event.target.closest?.('[data-room-complete]')) event.preventDefault(); });
function capture() {
if (!current) return null;
const active = document.activeElement, log = current.container.querySelector('.chat-log');
@@ -202,14 +358,16 @@
const keep = capture(), c = current;
let html = '<div class="chat-area"><p><a href="#/chat">Chat projects</a></p>';
if (c.error) html += `<p class="chat-error banner err" role="alert">${txt(c.error)}</p>`;
if (c.kind === 'thread') {
if (c.kind === 'room') {
html += c.refused ? heading('Room unavailable') : roomConversation();
} else if (c.kind === 'thread') {
const t = c.thread;
if (t && (c.session || c.legacyDefault) && !c.refused) html += conversation(t);
else html += heading('Thread unavailable');
} else if (c.kind === 'project') {
const p = c.projects.find(p => p.id === c.id);
html += heading(p?.name ?? 'Project unavailable');
if (p && !c.refused) html += threads(c.threads.filter(t => !t.shared)) + '<h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('thread', c.id);
if (p && !c.refused) html += roomOpenControl(c.id) + threads(c.threads.filter(t => !t.shared)) + '<h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('thread', c.id);
} else {
html += heading('Chat') + '<p>Business projects and your named assistant threads.</p><div class="chat-projects">' +
c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id && !t.shared))}</section>`).join('') + (c.projects.length ? '' : '<p>No projects yet.</p>') + '</div><h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('project', 'projects');
@@ -231,6 +389,7 @@
if (keep.field.start != null && typeof f.setSelectionRange === 'function') f.setSelectionRange(keep.field.start, keep.field.end);
}
} else if (navigated) c.container.querySelector('h1')?.focus({ preventScroll: true });
updateRoomCompletion();
}
function readError(err) {
return err.code === 'thread-not-owned' ? 'This thread is not available to this user.' :
@@ -239,6 +398,7 @@
}
async function render({ path, container, canSend, user = false, tester = false, navigated = false, next = false }) {
if (out()) return;
if (/^\/chat\/rooms(?:\/|$)/.test(path)) return renderRoom({ path, container, canSend, user, tester, navigated, next });
const match = /^\/chat(?:\/(projects|threads)\/([0-9a-f-]+)(?:\/sessions\/([0-9a-f-]+))?)?$/.exec(path);
controller?.abort(); controller = new AbortController();
const signal = controller.signal, g = ++generation;
@@ -389,6 +549,7 @@
}
document.addEventListener('input', event => {
if (!current || out()) return;
if (event.target.id === 'chat-room-body') { state(current.key).draft = event.target.value; updateRoomCompletion(); return; }
if (event.target.id === 'chat-operation') { const f = form('clear:' + current.key); if (f.pending) return; f.op = event.target.value; f.record = null; const button = document.getElementById('chat-cancel'); if (button) button.disabled = !current.canSend || f.pending || !f.op; }
if (event.target.id === 'chat-share-target') form('share:' + current.id).to = event.target.value;
if (event.target.id === 'chat-body') state(current.key ?? current.id).draft = event.target.value;
@@ -400,7 +561,7 @@
}
});
document.addEventListener('scroll', event => {
if (event.target.matches?.('.chat-log') && current?.kind === 'thread') {
if (event.target.matches?.('.chat-log') && ['thread', 'room'].includes(current?.kind)) {
const el = event.target;
state(current.key ?? current.id).follow = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
}
@@ -449,7 +610,9 @@
document.addEventListener('submit', event => {
if (!current || !event.target.id.startsWith('chat-')) return;
event.preventDefault();
if (event.target.id === 'chat-share-form') {
if (event.target.id === 'chat-room-send-form') {
void sendRoom();
} else if (event.target.id === 'chat-share-form') {
void changeShare('share', form('share:' + current.id).to ?? '');
} else if (event.target.id === 'chat-send-form') {
const id = current.id, session = current.session, key = current.key;
@@ -473,6 +636,9 @@
});
document.addEventListener('click', event => {
if (!current || out()) return;
if (event.target.id === 'chat-room-open') void openRoom();
const option = event.target.closest?.('[data-room-complete]');
if (option) completeRoom(option.dataset.roomComplete);
if (event.target.dataset.unshare) void changeShare('unshare', event.target.dataset.unshare);
if (event.target.id === 'chat-resume') void write('open', current.key, current.session ? { session: current.session } : {}, `threads/${current.id}/open`);
if (event.target.id === 'chat-cancel') void cancelClear();
+198
View File
@@ -0,0 +1,198 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { fixture } from './fixture.mjs';
import { browser } from './browser.mjs';
import { addressCases } from '../../bus/tests/address-cases.mjs';
const P = '10000000-0000-4000-8000-000000000001', Q = '10000000-0000-4000-8000-000000000002';
const T = '20000000-0000-4000-8000-000000000001', U = '20000000-0000-4000-8000-000000000002';
const M = '30000000-0000-4000-8000-000000000001', N = '30000000-0000-4000-8000-000000000002';
const A = '40000000-0000-4000-8000-000000000001', B = '40000000-0000-4000-8000-000000000002';
const C = '50000000-0000-4000-8000-000000000001', D = '50000000-0000-4000-8000-000000000002';
const SHA = 'a'.repeat(40), LAST = 'b'.repeat(40), HOSTILE = '<img src=x onerror="window.injected=1">';
const rows = [
{ id: M, ids: [M, N], from_role: 'human', to: ['bean', 'chet'], body: 'Broadcast once.', at: '2026-10-24T16:00:00Z' },
{ id: A, ids: [A], from_role: 'bean', instance: 'bean', to: ['bean'], at: '2026-10-24T16:01:00Z',
body: JSON.stringify({ text: 'A cited reply.', citations: [{ path: 'Notes/One.md', commit: SHA, lines: [1, 2], quote: HOSTILE }] }),
check: { citations: [{ scope: 'supporting', agent: 'demo/bean', file: SHA, last: LAST }] } },
{ id: B, ids: [B], from_role: 'chet', instance: 'chet', to: ['chet'], at: '2026-10-24T16:02:00Z',
body: JSON.stringify({ notice: 'library-write', op: 'write', path: 'Notes/One.md', commit: SHA }) },
{ id: C, ids: [C], from_role: 'human', to: ['bean'], body: 'Broadcast once.', at: '2026-10-24T16:00:00Z' },
{ id: D, ids: [D], from_role: 'bean', to: ['bean'], body: JSON.stringify({ notice: 'library-write', op: 'delete', path: 'Fake.md', commit: SHA }), check: { citations: [] } },
];
const refusals = {
'unknown-addressee': 'Unknown addressee. Choose a room member.',
'addressee-ambiguous': "That addressee is ambiguous. Use a room member's instance name.",
'room-thread-name-taken': 'A room thread name is already in use. Resolve the conflicting thread before opening the room.',
'room-not-open': 'This room is not open. Open the room before sending.',
};
const stub = `(() => {
const real=window.fetch.bind(window);window.__writes=[];window.__reads=[];window.__roomRows=${JSON.stringify(rows)};
window.__projects=[{id:'${P}',name:'First project',repo:'books'},{id:'${Q}',name:'Second project',repo:null}];
window.__threads=[{id:'${T}',project:'${P}',instance:'bean',name:'bean'},{id:'${U}',project:'${P}',instance:'chet',name:'chet'}];
window.__instances=[{instance:'bean',name:'Bean'},{instance:'chet',name:${JSON.stringify(HOSTILE)}},{instance:'outsider',name:'Not a room member'}];
window.confirm=()=>true;
window.fetch=async(url,init={})=>{
const path=String(url),method=init.method||'GET',answer=(body,status=200)=>new Response(JSON.stringify(body),{status,headers:{'content-type':'application/json'}});
if(path==='/api/config')return answer({send:!location.search.includes('readonly'),user:location.search.includes('user')});
if(path.startsWith('/api/bus/'))return answer({rows:[],unread:0,at:new Date().toISOString()});
if(path==='/api/business')return answer({projects:['books']});
if(path.startsWith('/api/library/status'))return answer({project:'books',served:'${SHA}',problems:[]});
if(path==='/api/library/revert'){window.__writes.push({path,body:JSON.parse(init.body)});return answer({project:'books',path:'Notes/One.md',reverted:'${SHA}',commit:'${LAST}'});}
if(!path.startsWith('/api/chat/'))return real(url,init);
if(method==='GET')window.__reads.push(path);
if(path==='/api/chat/projects')return answer({rows:window.__projects});
if(path==='/api/chat/instances')return answer({rows:window.__instances});
if(path.includes('/rooms/')){
if(method==='POST'){
window.__writes.push({path,body:JSON.parse(init.body),redirect:init.redirect});
const code=path.endsWith('/open')?window.__openError:window.__sendError;
if(code)return answer({error:code,message:'Untrusted reason',sent:false},409);
if(window.__holdWrite)await new Promise(r=>window.__releaseWrite=r);
if(window.__networkFail)throw new TypeError('offline');
if(path.endsWith('/open')){window.__readError=null;return answer({project:'${P}',threads:window.__threads.map(({id,instance,name})=>({id,instance,name}))});}
const ids=['60000000-0000-4000-8000-000000000001','60000000-0000-4000-8000-000000000002'];
window.__roomRows.push({id:ids[0],ids,from_role:'human',to:['bean','chet'],body:JSON.parse(init.body).body,at:'2026-10-24T17:00:00Z'});
return answer({ids,to:['bean','chet']});
}
if(window.__readError)return answer({error:window.__readError,message:'Untrusted reason'},409);
if(window.__holdRead){window.__signal=init.signal;await new Promise(r=>window.__releaseRead=r);}
return answer({rows:window.__roomRows,after:window.__roomRows.at(-1)?.ids.at(-1)||null,hasMore:false});
}
if(path.includes('/shares'))return answer({rows:[]});
if(path.endsWith('/sessions'))return answer({rows:[{id:'${T}',name:'default',default:true}]});
if(path.includes('/messages'))return answer({rows:[],after:null,hasMore:false});
if(path.startsWith('/api/chat/threads'))return answer({rows:window.__threads});
return answer({rows:[]});
};
})();`;
async function setup(t, { path = `/chat/rooms/${P}`, before = '', suffix = '' } = {}) {
const f = await fixture(); t.after(() => f.close());
const b = await browser(); t.after(() => b.close());
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub + before });
await b.navigate(`${f.base}/${suffix}#${path}`);
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expression)});})()`);
const text = selector => b.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent??null`);
const set = value => b.evaluate(`(()=>{const el=document.querySelector('#chat-room-body');el.value=${JSON.stringify(value)};el.focus();el.setSelectionRange(el.value.length,el.value.length);el.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`);
await wait(path.includes('/rooms/') ? 'document.querySelector(".chat-room")' : 'document.querySelector("#chat-thread-form")');
return { b, wait, text, set };
}
test('C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery', async t => {
const { b, wait, text } = await setup(t);
await wait('document.querySelector(".chat-cites")');
assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]").length'), 5);
assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]")[0].dataset.message'), M);
assert.match(await text('.chat-room-log > li:first-child .chat-message-meta'), /Human.*Bean/);
assert.equal(await text('.chat-scope'), 'supporting');
assert.equal(await text('.chat-agent-written'), 'agent-written: demo/bean');
assert.equal(await text('.chat-changed'), 'changed since');
assert.equal(await text('.chat-cites q'), HOSTILE);
assert.equal(await b.evaluate('!!document.querySelector(".chat-room img") || !!window.injected'), false);
assert.equal(await b.evaluate('document.querySelectorAll("[data-revert]").length'), 1);
assert.equal(await b.evaluate('window.__writes.length'), 0, 'reading never opens a room or wakes an agent');
await b.evaluate('document.querySelector("[data-revert]").click();true');
await wait('document.querySelector(".chat-revert-receipt")?.textContent.startsWith("Reverted")');
assert.deepEqual(await b.evaluate('window.__writes[0].body'), { project: 'books', commit: SHA });
});
test('C2-10c explicit project Open room calls only room.open before navigating', async t => {
const { b, wait } = await setup(t, { path: `/chat/projects/${P}` });
assert.equal(await b.evaluate('window.__writes.length'), 0);
await b.evaluate('document.querySelector("#chat-room-open").click();true');
await wait('document.querySelector(".chat-room")');
assert.deepEqual(await b.evaluate('window.__writes'), [{ path: `/api/chat/rooms/${P}/open`, body: {}, redirect: 'manual' }]);
});
test('C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly', async t => {
const { b, wait, set } = await setup(t);
await set('@be');
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-complete]")].map(e=>e.dataset.roomComplete)'), ['bean']);
await b.key('Enter');
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@bean ');
await set('@bean @ch');
await b.evaluate('document.querySelector("[data-room-complete=chet]").click();true');
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@bean @chet ');
const body = ' \t@bean\n@chet unchanged text ';
await set(body);
await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true');
await wait('window.__writes.length===1 && !document.querySelector("#chat-room-body").disabled');
assert.deepEqual(await b.evaluate('window.__writes[0]'), { path: `/api/chat/rooms/${P}/messages`, body: { body }, redirect: 'manual' });
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '');
assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]").length'), 5, 'no optimistic copies of a broadcast');
await wait('!document.querySelector("#refresh").disabled');
await b.evaluate('document.querySelector("#refresh").click();true');
await wait('document.querySelectorAll(".chat-room-log > li[data-message]").length===6');
const sentSelector = '.chat-room-log > li[data-message="60000000-0000-4000-8000-000000000001"]';
assert.equal(await b.evaluate(`document.querySelectorAll(${JSON.stringify(sentSelector)}).length`), 1);
});
test('C2-10c browser leading tag reader matches every broker parseAddress vector', async t => {
const { b, set } = await setup(t);
for (const [label, input, expected] of addressCases) {
await set(input);
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-tag]")].map(e=>e.dataset.roomTag)'), expected.tags, label);
}
await set('ordinary prose @be');
assert.equal(await b.evaluate('document.querySelectorAll("[data-room-complete]").length'), 0);
assert.equal(await b.evaluate('window.__writes.length'), 0);
});
test('C2-10c room refusals show fixed lines, preserve drafts and never retry', async t => {
const { b, wait, set, text } = await setup(t);
for (const code of ['unknown-addressee', 'addressee-ambiguous', 'room-not-open']) {
await set('@unknown hello');
const count = await b.evaluate('window.__writes.length');
await b.evaluate(`window.__sendError=${JSON.stringify(code)};document.querySelector('#chat-room-send-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));true`);
await wait(`window.__writes.length===${count+1} && !document.querySelector('#chat-room-body').disabled`);
assert.equal(await text('#chat-room-receipt'), refusals[code]);
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@unknown hello');
}
await b.evaluate("window.__openError='room-thread-name-taken';document.querySelector('#chat-room-open').click();true");
await wait(`document.querySelector('#chat-room-open-receipt')?.textContent===${JSON.stringify(refusals['room-thread-name-taken'])}`);
assert.equal(await text('#chat-room-open-receipt'), refusals['room-thread-name-taken']);
});
test('C2-10c read-only room does not offer a send or an implicit open', async t => {
const { b, set } = await setup(t, { suffix: '?readonly' });
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").disabled'), true);
assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true);
await set('not allowed');
await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true');
assert.equal(await b.evaluate('window.__writes.length'), 0);
});
test('C2-10c late reads and sign-out cannot repaint private room state', async t => {
const { b, wait, set } = await setup(t);
await set('private room draft');
await b.evaluate('window.__holdRead=true;document.querySelector("#refresh").click();true');
await wait('typeof window.__releaseRead==="function"');
await b.evaluate(`location.hash='#/chat/projects/${Q}';true`);
await wait('document.querySelector("#chat-thread-form")');
assert.equal(await b.evaluate('window.__signal.aborted'), true);
await b.evaluate('window.__holdRead=false;window.__releaseRead();true');
await b.evaluate('document.dispatchEvent(new Event("mosaic-signed-out"));true');
assert.equal(await b.evaluate('!!document.querySelector(".chat-room")'), false);
assert.equal(await b.evaluate('document.body.textContent.includes("private room draft")'), false);
});
test('C2-10c quiet members complete in an empty room, and Escape closes suggestions', async t => {
const { b, set } = await setup(t, { before: 'window.__roomRows=[];' });
await set('@');
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-complete]")].map(e=>e.dataset.roomComplete)'), ['bean', 'chet']);
await b.key('ArrowDown'); await b.key('Tab');
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@chet ');
await set('@b'); await b.key('Escape');
assert.equal(await b.evaluate('document.querySelector("#chat-room-completion").hidden'), true);
assert.equal(await b.evaluate('window.__writes.length'), 0);
});
test('C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft', async t => {
const { b, wait, set, text } = await setup(t);
await set('one pending send');
await b.evaluate('window.__holdWrite=true;document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true');
await wait('typeof window.__releaseWrite==="function"');
await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true');
assert.equal(await b.evaluate('window.__writes.length'), 1);
await b.evaluate('window.__holdWrite=false;window.__releaseWrite();true');
await wait('!document.querySelector("#chat-room-body").disabled');
await set('keep this uncertain draft ');
await b.evaluate('window.__networkFail=true;document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true');
await wait('document.querySelector("#chat-room-receipt").textContent.includes("Outcome unknown")');
assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), 'keep this uncertain draft ');
assert.equal(await b.evaluate('window.__writes.length'), 2);
assert.match(await text('#chat-room-receipt'), /Nothing was retried/);
});
+114
View File
@@ -0,0 +1,114 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { startServer } from '../src/serve.mjs';
import { createChat } from '../src/chat.mjs';
import { BusReadError } from '../src/bus.mjs';
const P = '10000000-0000-4000-8000-000000000001', T = '20000000-0000-4000-8000-000000000001';
const M = '30000000-0000-4000-8000-000000000001', N = '30000000-0000-4000-8000-000000000002';
const headers = { host: 'console.example', 'x-user': 'jason', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
const lines = {
'unknown-addressee': 'Unknown addressee. Choose a room member.',
'addressee-ambiguous': "That addressee is ambiguous. Use a room member's instance name.",
'room-thread-name-taken': 'A room thread name is already in use. Resolve the conflicting thread before opening the room.',
'room-not-open': 'This room is not open. Open the room before sending.',
};
async function setup(t, { enabled = true } = {}) {
const calls = [], state = { value: null };
const chat = createChat({ call: async (verb, args) => {
if (verb === 'plugins') return { plugins: { channel: ['webui'] } };
calls.push([verb, args]);
if (state.value instanceof Error) throw state.value;
return state.value;
} });
const server = await startServer({ port: 0, preview: { host: 'console.example', header: 'x-user', user: 'jason' }, send: enabled ? async () => ({}) : null, chat, log: () => {} });
t.after(() => new Promise(r => { server.close(r); server.closeIdleConnections(); }));
const call = (action, { method = 'GET', value, hs = headers, query = '' } = {}) => new Promise((resolve, reject) => {
const req = request({ host: '127.0.0.1', port: server.address().port, path: `/api/chat/rooms/${P}/${action}${query}`, method, headers: hs }, res => {
let text = ''; res.on('data', c => text += c); res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(text) }));
});
req.on('error', reject); req.end(value === undefined ? undefined : JSON.stringify(value));
});
return { calls, state, call, chat };
}
test('C2-10c seam exposes three fixed room calls without thread.messages fanout', async t => {
const f = await setup(t);
for (const [method, args, verb] of [['roomOpen', { project: P }, 'room.open'], ['roomRead', { project: P, after: M, limit: 2 }, 'room.read'], ['roomSend', { project: P, body: '@bean exact\ntext ' }, 'room.send']]) {
assert.equal(typeof f.chat[method], 'function');
f.state.value = [];
await f.chat[method](args);
assert.deepEqual(f.calls.at(-1), [verb, args]);
}
assert.deepEqual(f.calls.map(c => c[0]), ['room.open', 'room.read', 'room.send']);
});
test('C2-10c HTTP preserves original send, member receipt and the last grouped paging id', async t => {
const f = await setup(t);
f.state.value = { project: P, threads: [{ id: T, instance: 'bean', name: 'bean' }] };
const open = await f.call('open', { method: 'POST', value: {} });
assert.equal(open.status, 200); assert.equal(open.body.project, P);
assert.deepEqual(f.calls.at(-1), ['room.open', { project: P }]);
const body = ' \t@bean\n exact text ';
f.state.value = { ids: [M, N], to: ['bean', 'chet'] };
const sent = await f.call('messages', { method: 'POST', value: { body } });
assert.equal(sent.status, 200); assert.deepEqual(sent.body.ids, [M, N]);
assert.deepEqual(f.calls.at(-1), ['room.send', { project: P, body }]);
const check = { citations: [{ scope: 'supporting', agent: 'demo/bean', file: 'a'.repeat(40), last: 'b'.repeat(40) }] };
f.state.value = [{ id: M, ids: [M, N], to: ['bean', 'chet'], body: 'one', check }];
const page = await f.call('messages', { query: `?after=${T}&limit=1` });
assert.equal(page.status, 200); assert.equal(page.body.after, N); assert.equal(page.body.hasMore, true);
assert.deepEqual(page.body.rows[0].check, check);
assert.deepEqual(f.calls.at(-1), ['room.read', { project: P, after: T, limit: 1 }]);
});
test('C2-10c room write fences and strict keys reject before transport', async t => {
const f = await setup(t), off = await setup(t, { enabled: false });
assert.equal((await off.call('open', { method: 'POST', value: {} })).status, 403);
for (const value of [{ project: P }, { owner: 'other' }, null, [], { body: 'x' }])
assert.equal((await f.call('open', { method: 'POST', value })).status, 400);
for (const value of [{}, { body: '' }, { body: 'x', to: ['bean'] }, { body: 'x', project: P }])
assert.equal((await f.call('messages', { method: 'POST', value })).status, 400);
for (const query of ['?limit=0', '?limit=201', '?after=x', '?project=other', `?after=${M}&after=${N}`])
assert.equal((await f.call('messages', { query })).status, 400);
for (const hs of [{ ...headers, origin: 'https://evil.example' }, { ...headers, 'sec-fetch-site': 'cross-site' }, { ...headers, 'x-user': 'other' }])
assert.ok([401, 403].includes((await f.call('open', { method: 'POST', value: {}, hs })).status));
assert.deepEqual(f.calls, []); assert.deepEqual(off.calls, []);
});
test('C2-10c all four room refusals are409 with fixed text, never transport prose', async t => {
const f = await setup(t);
for (const [code, message] of Object.entries(lines)) {
f.state.value = new BusReadError(code, 'private transport reason');
const r = await f.call(code === 'room-thread-name-taken' ? 'open' : 'messages', { method: 'POST', value: code === 'room-thread-name-taken' ? {} : { body: '@unknown hello' } });
assert.equal(r.status, 409, code); assert.equal(r.body.error, code); assert.equal(r.body.message, message); assert.equal(r.body.sent, false);
assert.equal(JSON.stringify(r.body).includes('private transport reason'), false);
}
assert.equal(f.calls.length, 4);
});
test('C2-10c malformed/lost write receipts are unknown and never retried', async t => {
const f = await setup(t);
for (const value of [null, { ids: [M], to: [] }, { ids: ['bad'], to: ['bean'] }, new Error('socket closed')]) {
f.state.value = value;
const before = f.calls.length, r = await f.call('messages', { method: 'POST', value: { body: 'hi' } });
assert.equal(r.status, 503); assert.equal(r.body.error, 'outcome-unknown'); assert.equal(r.body.sent, 'unknown');
assert.equal(f.calls.length, before + 1);
}
});
test('C2-10c room receipts reject coerced member keys and duplicate grouped ids', async t => {
const f = await setup(t);
f.state.value = { project: P, threads: [{ id: T, instance: 17, name: 17 }] };
const opened = await f.call('open', { method: 'POST', value: {} });
assert.equal(opened.status, 503); assert.equal(opened.body.sent, 'unknown');
for (const row of [{ id: M, ids: [M, M], to: ['bean', 'chet'], body: 'x' }, { id: M, ids: [M, N], to: ['bean', 'bean'], body: 'x' }]) {
f.state.value = [row];
const read = await f.call('messages');
assert.equal(read.status, 502); assert.equal(read.body.error, 'invalid-response');
}
});
test('C2-10c granted-user room calls never borrow the owner transport', async () => {
const owner = [], user = [];
const chat = createChat({ owner: async (...a) => { owner.push(a); return { plugins: { channel: ['webui'] } }; },
call: async (...a) => { user.push(a); throw new BusReadError('human-required'); } });
await assert.rejects(chat.roomRead({ project: P, limit: 100 }), { code: 'human-required' });
assert.deepEqual(owner, [['plugins', {}]]);
assert.deepEqual(user, [['room.read', { project: P, limit: 100 }]]);
});