Compare commits

..
Author SHA1 Message Date
Lena a1321f8f90 Untested: record R7 reload ordering and slot boundaries (#1802) 2026-10-23 14:31:47 -05:00
Lena e855f82e18 Untested: preserve R7 shell cleanup during mutant runs (#1802) 2026-10-23 14:30:20 -05:00
Lena f06a1f78cf Untested: row 296 scratch two-instance acceptance (#1802) 2026-10-23 14:24:24 -05:00
jason.woltje ad84c1e45c row 296 handoff: FED R7 design notes to lena/row296 (mos-1503, Q245)
Quackerjack stops row 296 here. BUILD.md holds the verified reads, the proposed
harness design, the open decisions for Lena, and the R2 learning that R7 depends on.
No test or code changes in this commit.
2026-10-23 14:10:41 -05:00
65 changed files with 1048 additions and 5474 deletions

No files matched your search

+5 -4
View File
@@ -5412,8 +5412,9 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
2026-10-23 | Fethry | Row 225 C2-10b (#1726), handoff recovery. Before: Morgana's untested candidate 9a131e956 replaces renderItems with renderReply and adds Console citation/notice/status rendering and status/revert routes. Continuing that exact history on fethry/row225; preserving A1-A5 and the accepted file deviations. Remaining: confirm limits contract, review/test candidate under Sage CLEAR, obtain Mos OK through Sage for inherited old-test changes, manifest and Dewey review. No Node/tests run in this session and no green claim.
## 2026-10-23 — Lena, row 296 / #1802 FED R7 start
2026-10-23 | Fethry | Row 225 T1 lane A (sage-3641/3647). Before: nested replyLimits fallback/problem omitted, wrong-project revert receipt accepted, invalid library mapping hidden. After RED: these three local behaviors corrected; three assertions in the new handoff test files corrected against existing route/mapping/Boolean behavior, each explained in BUILD.md. Whole-file GREEN: renderer 9/0, routes 13/0, browser 9/0; full library 81/0, channel-proof 2/0. Approved old-test numstats remain 80/66 and 3/1. Full webui/build and full integration gate have not run. Isolated mutants next within the authorized 19:38Z hard stop; no completion claim.
2026-10-23 | Fethry | Row 225 T1 expanded evidence complete (sage-3647). Full library81/0 and channel-proof2/0; final targeted renderer9/0, routes13/0, browser9/0. Isolated kit14/14 assertion kills on507901277, every target restored/hash-checked, candidate hashes unchanged. First attempt stopped at a wait-based failure, recorded as not killed; corrected the new test's repaint wait, baseline9/0, and reran the whole kit. No processes left; lane A returned around19:29Z. Source manifest for Dewey prepared; full webui/build and Sage's integration/scripts gate still pending. Raw logs preserved in scratch, normalized display copies and raw SHA256 provenance in agent evidence.
Before: R4 and R6 landed in base 45dca69f6; no scratch two-instance acceptance script.
Plan: preserve Quackerjack handoff c6ab9bdc9, add isolated script/CLI test harness with real
Console proof in transient user services, map assertions and two mutants for Filbert.
Untested: all Node/tests and shared gate wait for Sage CLEAR; no production or live state changes.
-376
View File
@@ -1,376 +0,0 @@
# Row 225 (C2-10b) Console chat: replies, citations, write notices, status line
Owner: Morgana, from sage-3582 (18:53Z 10-23). Base: origin/refactor 45dca69f6. Branch: morgana/row225.
Brief: docs/plans/2026-10-20_case2-rows.md, C2-10b. Design: agents/gosalyn/work/case2/DESIGN.md 7.4,
5.2 (the reply schema and the check result) and the write notice and `library.revert` (about line 286).
## Ruling on the dependency gap (sage-3591)
225 builds against the design contracts. `renderReply` stays pure, and the caller supplies the answers
it can't know. The revert route proxies to the bus verb and shows the refusal code until the verb
exists. Landing order: 225 lands after 217 (C2-06), not after 288 (C2-02). Sage adds `after 217` to
row 225. Before 288 lands, the Revert button gets the bus's refusal and shows its code, as the brief
allows.
## A1. The check result I assume (to 217, Launchpad)
The broker stores one check result with each agent-to-human message the guard passes. The Console reads
it on the message row that `/api/chat/threads/<id>/messages` returns, under the key `check`:
```json
{
"check": {
"citations": [
{ "scope": "canon", "agent": null },
{ "scope": "supporting", "agent": "acme/scribe" },
{ "scope": "web", "agent": null }
]
}
}
```
- `check` is `null` or absent on a message the guard didn't check: a human's message, a free-text reply,
a notice, or a message from before 217.
- `check.citations` has one entry per entry of the body's `citations`, in the same order.
- `scope` is one of `canon`, `playground`, `supporting` and `web`. A url cite is `web`.
- `agent` is the `Mosaic-Agent` trailer value, `<business>/<instance>`, from
`git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>`, or `null` when the
file's last commit at `commit` carries none. A url cite has `agent: null`. The Console shows the
agent-written label when `agent` is a nonempty string, and names the agent.
The Console trusts nothing in the body for scope or label. A cite with no matching check entry renders
with no badge and no label. It doesn't fall back to a scope it worked out itself.
If 217 uses other names, I change mine to theirs before either of us pushes (sage-3591 item 1).
## A2. The revert verb I assume (to 288, Honker)
- Verb: `library.revert`. A human verb (design M4): from an agent session it refuses `human-required`,
like `library.status` (`broker.mjs:5133`).
- Args: `{ "project": <business project name>, "commit": <40 lowercase hex> }`. The design writes
`library.revert {commit}`. `project` is added to match the other library verbs, and like theirs it is the
key of `business.projects` (`mirror.mjs:31`), which is the chat project's `repo` link, not its UUID. Both are required, and no other key
is allowed (`invalid-request`), as the other library verbs check with `keys(args, ...)`.
- Result: `{ "project", "path", "reverted": <the commit given>, "commit": <the new commit> }`.
- Refusal codes the Console shows by name:
- `library-revert-not-agent`: `commit` has no `Mosaic-Agent` trailer for this business, or isn't
reachable from the served commit.
- `stale-base`: a later commit changed that path.
- `human-required`, `invalid-request`, `project-not-configured`.
- Before 288 lands, the bus refuses the verb (expected `unknown-verb`, `broker.mjs:6578`), and the Console
shows that code.
- HTTP status in the Console (`packages/webui/src/bus.mjs` `busStatus`): `stale-base` and
`library-revert-not-agent` map to 409. Every other code keeps the existing mapping.
## A3. The status line
`GET /api/library/status?project=<id>` proxies `library.status {project}` through the existing
`libraryStatus` option. `cli.mjs:110` already passes that option, but `startServer` doesn't read it.
The status line shows:
- the served commit (7 characters) and its age;
- `library-diverged` when the sync state names it;
- the fetch error code;
- the reply limits in force, and the keys that fell back;
- the count of R-problems, linking to the list.
It reads the fields `readSyncState` returns at the base (`packages/library/src/mirror.mjs:232`). It shows
any field 217 adds (the limits) only when present.
## A4. "Changed since" (ruled sage-3596)
Sage ruled that 217 supplies `last`. On a `thread.messages` row that carries `check`, each path cite's
entry gets `last`, the file's last commit at the served tip, read when the message is read. When `last`
is absent the Console marks nothing changed, and a test pins that.
One more field is needed, raised in morgana-0200. The cited `commit` is a library commit, not the
file's own last commit, so a cite made at a commit that didn't touch the file would differ from `last`
while the file is unchanged. I asked 217 to also store `file`, the file's last commit at the cited
commit. That's `%H` from the same `git log -1 <commit> -- <path>` call that reads the trailer, so it
costs no extra git call. The Console marks a cite changed when both `file` and `last` are 40-hex and
differ. The cited commit is reachable from the served one (5.2), so `file` is an ancestor of `last`,
and differing means newer. A missing or malformed `file` or `last` marks nothing.
The entry the Console reads, in full: `{ scope, agent, file?, last? }`.
## A5. `no-cited-answer` retires
`renderItems` and its `NOTICES` (`no-cited-answer`, `no-answer`, `turn-failed`) go. 217 sends the
runner's fixed texts as plain bodies (design 5.2, "Runner texts"), and renderReply shows those as free
text. Design 7.4 names no other notice than `library-write`, so nothing is lost (sage-3596 item 3).
## Files
Both deviations below were accepted in sage-3596 and are named in the manifest for Dewey. The brief owns these:
- `packages/library/src/render.mjs`: `renderReply` replaces `renderItems`.
- `packages/library/src/index.mjs`: the export.
- `packages/library/README.md`: the section.
- `packages/library/tests/render.test.mjs`.
It also owns `bus.js`, its stylesheet, `serve.mjs` (only the two routes) and new webui tests. Two
deviations, both recorded here:
- The chat view's message list is `packages/webui/src/public/chat.js:85` (`window.MosaicChat`), not
`bus.js`. The reply, notice and Revert rendering goes in `chat.js` and `chat.css`. The status line
goes in the chat thread view, so it is also in `chat.js`.
- `packages/webui/src/console-bus.mjs` and `cli.mjs` each get one `libraryRevert` line, beside the
existing `libraryStatus` wiring.
## Overlap with row 220 (C2-10a, Researcher, 3ef6da90d)
220 changes one line each in the webui tests discord.test.mjs, fixture.mjs, history-fixture.mjs,
relaunch.test.mjs and return-flow.test.mjs. Its other paths are in control-board, conversation, discord
and business. 225 doesn't touch those five files or those packages. Its webui tests are new files.
## Handoff to Fethry (mos-1501, Q245)
The row moved to Fethry (Codex, fethry/row225). Everything below is **untested**: no suite or single file
has run on it, since a run needs Sage's CLEAR. Each changed file passes `node --check`.
Done in the handoff commit:
- `render.mjs` `renderReply(body, {check, changed, name})`; index export; README section;
`render.test.mjs` rewritten. `renderItems` is gone from every package.
- `serve.mjs`: `/render.mjs` static entry (owner and user file maps), `GET /api/library/status`
(one `project` query, answer must name that project), `POST /api/library/revert` (save's boundary,
strict `{project, commit}`, refusal shows the code, lost or unusable answer is `outcome-unknown`,
never retried). Both are nulled for a granted user, and `userRoute` keeps them owner-only. The tester
listener refuses `libraryRevert`.
- `bus.mjs`: `library-revert-not-agent: 409`. That re-pins `packages/webui/src/bus.mjs` in
`packages/bus/tests/channel-proof.test.mjs`, with a comment line, as row 254 did. It's a third deviation,
in another package's test.
- `console-bus.mjs` and `cli.mjs`: `libraryRevert`, beside `libraryStatus`.
- `chat.js`: dynamic `import('/render.mjs')` (CSP is `script-src 'self'`); body text until it loads.
Cites with a scope badge, the agent-written label, "changed since" from `file`/`last`, links only for
https with `rel="noopener noreferrer"`. A notice gets a Revert button behind `confirm()`. The library
status line goes under the thread heading.
- `chat.css`: cite, badge, notice and status styles.
- New tests: `packages/webui/tests/library-routes.test.mjs` and
`packages/webui/tests/chat-replies-browser.test.mjs`. 220's five files are untouched.
Decisions made in the build:
- The library name is the chat project's `repo`. A project with no repo reads no status and its
Revert does nothing.
- These codes hide the status line: `project-not-declared`, `library-mapping-invalid`,
`project-not-configured`, `not-configured` and `owner-only`.
- Otherwise every code-repo thread, and every granted user, would show "unavailable".
- Cost: a broken library mapping also hides the line. `library.status` from the CLI still shows it.
- "Age" is the time since `lastFetch` ("fetched 5 min ago"), not the served commit's age, which the
status doesn't carry.
- The limits fields are assumed to be `replyLimits {requireCitation, citationsPerReply}` and
`replyLimitsFallback [keys]`, and are shown only when present. **Not agreed with 217.** Confirm with
Beakley and rename.
- A notice body's shape is all the renderer checks. An agent in a business without a library could
send a body that looks like a notice. The page would show it, but `library.revert` still refuses it.
Left:
1. Get CLEAR and run the webui and library suites, `channel-proof.test.mjs`, and the two new files whole.
The browser test's selectors and waits have never run.
2. Agree the limits field names with 217 (Beakley).
3. Granted-user R6 G5 (`granted-user.test.mjs:252`) lists owner verbs by hand. Add `library.revert`
once 288 lands.
4. BUILD.md: the mutant list and the mos-1275 timeout plan.
5. The manifest names the three deviations: chat.js and chat.css in place of bus.js, the console-bus and
cli lines, and the channel-proof re-pin.
## Fethry continuation (2026-10-23, Q245)
Owner: Fethry, thread 14f80e3c-ace7-4793-a577-e1275ca09b0b. Branch fethry/row225.
This file carries Morgana's complete BUILD.md above unchanged, recovered from 9a131e956
(base 45dca69f6). Morgana stopped editing 225 at that handoff. Issue #1726; Dewey reviews.
Sage owns queue moves, test slots, gate and landing. No Node/tests until Sage CLEAR
(sage-3611); commits/pushes before the gate are explicitly Untested. No credential files read.
### Confirmed interfaces and rulings
- Beakley beakley-0004 confirms ordered check.citations {scope, agent, file, last}; a missing
or malformed file/last means no changed marker. Fetched age accurately describes lastFetch.
- Beakley's concrete library.status shape is replyLimits:
{requireCitation, citationsPerReply, fallback: [keys], problem: 'reply-limits-unreadable' | null}.
The inherited top-level replyLimitsFallback assumption is superseded. Show nested fallback and
problem, with a browser regression test for each. No broker or policy change in this row.
- Bubba bubba-0002 confirms A2 and the exact broker notice shape. fethry-0005 forwards the
project-key correction: project is chat project.repo, not the chat UUID.
- Keep the inherited ordinary-code-repo/granted-user status hiding behavior for now, but a
library-mapping-invalid error should stay visible: hiding an invalid mapping hides diagnosis.
Confirm this local UI choice through regression coverage and tell Sage/Dewey.
### Continuation plan and acceptance evidence
Spec: docs/plans/2026-10-20_case2-rows.md C2-10b; design 5.2 and 7.4;
standing rulings sage-3591, sage-3596 and sage-3611. Implement inline in this existing isolated
T3 worktree; use the existing candidate and tests, never restart the implementation.
1. Static audit the handoff against the contracts and inherited tests. Add failing coverage for
nested limits fallback/problem, wrong-project revert receipts, missing file/last and invalid
mapping visibility. Existing tests require Mos approval through Sage before further edits.
2. On Sage CLEAR, run the new files whole and library renderer tests with per-test deadlines;
record RED before changing uncovered behavior. Fix only demonstrated local defects. Run
targeted GREEN, then the full relevant suites only within the authorized slot.
3. Record exact commands, timings and output paths. Prepare mutant kill list below; run only on
CLEAR, using isolated copies so the candidate is never temporarily changed for mutation.
4. Freeze a manifest containing all source/test/doc changes and all three handoff deviations.
Push only fethry/*, as additional commits. Send exact head/results to Dewey and Sage; cc Mos
on pushes/blockers. Sage performs queue moves, gate and landing. No merge/release/live changes.
### Existing test changes requiring Mos OK through Sage (mos-1272)
Diff against 45dca69f6, inherited from Morgana 9a131e956:
| File | Added | Removed | Reason |
| --- | ---: | ---: | --- |
| packages/library/tests/render.test.mjs | 80 | 66 | Brief requires retiring renderItems and replacing its tests with renderReply coverage |
| packages/bus/tests/channel-proof.test.mjs | 3 | 1 | bus.mjs gains library-revert-not-agent:409; refresh that source hash and document why |
No further edits to these files pending approval. No edits to row220's five tests.
The new webui test files are additions against the row base.
### Timeout plan (mos-1275)
Every Node test run uses --test-concurrency=1 and --test-timeout=30000, under an outer timeout.
New library/route files: outer 120s each; new browser file: outer 240s. Full library and channel-proof:
outer 180s; full webui: outer 900s, MOSAIC_REQUIRE_EDITOR_BUILD=1. Full webui requires its own
packages/webui npm ci per agents/README.md and Sage CLEAR. Output redirects to an evidence log;
inspect tail, failure names and summary, never claim green from an exit alone. A timeout is a
failed run: stop, report the exact command/phase/last output, and diagnose before retrying.
Sage's gate owns every scripts/test-*.sh; no implicit slot or test authority from this plan.
### Mutant plan (pending CLEAR)
- M1: drop a path badge or the supporting label; cite browser assertions must fail.
- M2: allow http/javascript links; renderer and browser safe-link assertions must fail.
- M3: compare cited library commit to last instead of file to last, or treat a missing file/last as
changed; changed-since assertions must fail, including same-file/different-library-commit.
- M4: ignore nested fallback/problem; status browser tests must fail.
- M5: remove a revert fence (send, identity, exact Origin or Fetch Metadata), strict key/commit
validation or wrong-project receipt validation; route tests must fail with zero unexpected calls.
- M6: retry a refused/unknown revert; single-call route/browser assertions must fail.
- M7: remove granted-user library-route fence; owner-only route coverage must fail.
Progress: handoff and interfaces recovered; candidate untested. No completion claim.
Static evidence correction: the first pushed patch artifact used ordinary context lines, whose
single-space blank context lines make git diff --check report whitespace in the artifact. Regenerated
inherited-old-tests.patch with --unified=0; it is the same exact two-file change, applicable with
git apply --unidiff-zero. No source or existing test changed for this correction.
Mutation kit authored at mutants.py (11 concrete mutations across M1-M7), Python AST parse only.
It archives the committed packages plus their tracked fixture dependencies into disposable snapshots,
links the existing dependency install read-only in practice (tests do not install), enforces exactly
one mutation anchor and requires an assertion failure for a kill. It records source hashes before
and after. It has not run; anchors for the planned fixes intentionally require those fixes first.
No Node/tests/build/browser work has run in Fethry's session pending Sage CLEAR.
### Old-test approval and preservation condition (mos-1510, sage-3623)
Mos approves render.test.mjs +80/-66 and channel-proof.test.mjs +3/-1 exactly. No additional edits
are permitted to either file without a new Mos OK through Sage. The condition on the renderer:
- The old malformed-body fixed-line behavior no longer applies. Design 5.2 now says any body
outside the cited-object schema is free text whose text is the whole body. Rejecting it into a
fixed line would hide human/free-text/older messages and the four fixed runner texts. The new
render.test.mjs tests “free text renders as itself with no citations” and “a body without the
reply or notice shape is free text: the whole body, no citations” explicitly cover this new rule.
- The old no-options path-badge behavior still applies. The new render.test.mjs test “without a
check result a path cite has no badge, no label and is never changed” covers it, and the browser
test checks an unchecked path cite has no badge/label. No path inference fallback was added.
Dewey checks both against the frozen old-test delta. The two numstats remain 80/66 and 3/1.
### Test slot and shared protocol (sage-3623, beakley-0006)
Targeted lane A slot is planned around 19:47Z, after Gosalyn's 226 run and Sage's queue gap.
Wait for explicit CLEAR; the scheduled time alone grants nothing. Hard stop 20:00Z for Sage's
226 gate. Boyd's lane B bus/cli/business work is disjoint and does not count as a busy lane A.
The three requested files are the library renderer test and the two new webui files, whole/serial.
Beakley confirms reply-protocol.mjs exports only GUARD_CODES (the seven guard refusal codes) and
FIXED_TEXTS {empty, failed, uncited, tooLong}. It has no scope/notice constants or validator.
renderReply defines none of those codes or runner texts: it already renders the fixed bodies as free
text. It therefore needs no protocol import and no extra browser module route (beakley-0006).
Do not add unused imports or redefine those shared codes/texts later.
2026-10-23T19:21:07Z dependency update (bubba-0006): Bubba's first static row288 push is
origin/bubba/row288 c867cada9, explicitly Untested with no Node run. A2 library.revert uses the
business.projects key; broker notice retains the exact agreed body. Bubba adds librarySave only
in console-bus.mjs/cli.mjs and leaves libraryRevert wiring owned by 225. Preserve both callback
changes when Sage integrates. No dependency merge or test run performed here; 225 still lands
after 217 and may show unknown-verb until 288 lands.
## T1 targeted RED to GREEN (sage-3641, 2026-10-23T19:23Z)
All three files ran whole and serial, with TMPDIR=/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2,
--test-concurrency=1 --test-timeout=30000 and outer timeout -k 10 (120s renderer, 120s routes,
240s browser). Initial renderer 9/0 (0.105s), routes 9/3 (0.226s), browser 5/3 (5.630s).
Final renderer 9/0 (0.105s), routes 12/0 (0.212s), browser 8/0 (5.284s). Logs in evidence/.
Production fixes after RED:
- Reject a library.revert receipt with the wrong project as outcome-unknown; no retry.
- Read replyLimits.fallback and display replyLimits.problem per beakley-0004/0006.
- Show library-mapping-invalid in the status line; its hidden-status branch is covered by the
browser refusal matrix once the nested-limits assertion passes. This diagnosis was made statically;
the first RED exits that matrix at the preceding fallback assertion, so visibility itself has only
a GREEN run, unlike the fallback/problem and wrong-project fixes.
Three assertion corrections in NEW handoff files (not pre-existing tests, per sage-3647):
1. library-routes.test.mjs: POST /api/library/status in preview mode expects 403, because the existing
global preview write fence runs before the GET-only route and returns read-only. The route remains
GET-only and no callback fires on that POST. Do not weaken or bypass the fence to get 405.
2. library-routes.test.mjs: project-not-declared expects 400, which busStatus already maps at the
row base. The inherited 502 expectation contradicted that existing map. No production map change.
3. chat-replies-browser.test.mjs: coerce the whole “img element OR window.injected” expression to a
Boolean. With no injected node/code the old expression returns undefined, not false. Assertions
still require neither DOM injection nor executed handler. All preceding cite/link assertions pass.
Frozen pre-existing test deltas still render.test.mjs 80/66 and channel-proof.test.mjs 3/1.
No Node/Chromium process with this slot TMPDIR remained after targeted tests (checked /proc).
Expanded CLEAR sage-3647: full library, then channel-proof whole, then isolated mutants that fit;
still hard stop 19:38Z. No full webui suite, npm ci or editor build authorized in this window.
Expanded-slot evidence: full library 81/0 (9.179s), channel-proof 2/0 (0.573s), whole/serial,
--test-timeout=30000, outer timeout -k 10 180, same TMPDIR. Logs copied to evidence/.
To exercise invalid-mapping visibility independently, added its own browser case and temporarily
restored only the hidden-mapping branch. Whole browser RED: 7/2 (8.810s); the direct assertion sees
null instead of the diagnostic, and the refusal-matrix wait also fails. Finally restored the exact
corrected source before the next run. This supersedes the earlier visibility “GREEN only” note.
Final targeted coverage before mutation: browser 9/0 (6014.970ms runner duration), routes 13/0.
Added an independent send-off test with a configured libraryRevert callback: absence of --send
still refuses with zero callback calls. This makes the send fence independently mutation-testable.
The kit now has 14 concrete mutations, including agent-label, unreadable-limits problem and send.
Each isolated target is restored to its candidate SHA256, and worktree hashes are checked before
continuing to the next mutant (sage-3647). Runner uses TAP so assertion failures can be distinguished
from timeouts/import/process failures. Full webui/build and the integration gate remain pending.
Dependency update beakley-0008: row217 first push origin/beakley/row217 c25f5b8da is UNTESTED,
continued from Launchpad e25ef0373. No dependency merge here; its protocol/module mount belongs to
217 and renderReply needs none of those codes/texts. Integration acceptance remains Sage's gate.
First isolated-mutant attempt stopped honestly at M3-wrong-commit: M1 badge, M1 agent label and M2
https-only link mutants were killed by assertions; M3 failed via the browser's expected-state wait,
so the strict runner did NOT count it as a kill. Every target restored/hash-checked and candidate
hashes unchanged. Logs retained in evidence/mutants-attempt1/. Corrected the new browser test to wait
for a completed repaint (independent of desired output), then assert changed-marker count. Whole
browser baseline remains 9/0. No production change. Re-run the whole isolated kit against this test.
## T1 complete: isolated mutants and source-review handoff
sage-3647 final kit on source candidate 507901277: 14/14 assertion kills, no timeout/import/process
failure counted. Each mutant matched one anchor, restored the target SHA256 and checked the three
candidate source hashes before the next. evidence/mutants-final/summary.json records every result;
final worktreeUnchanged=true. Logs name the failing tests, including M3's direct assertion after the
refresh wait correction. /proc inspection found no remaining Node/Chromium process with the slot
TMPDIR. Returned lane A to Sage as fethry-0016 at about19:29Z, before the19:38Z hard stop.
Checked-in TAP display copies remove trailing whitespace so the diff is clean. Their raw SHA256
values are recorded in evidence/raw-log-provenance.json; original byte-for-byte logs remain under
/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2. This changes evidence presentation only, never tests.
MANIFEST.md and manifest.r1.sha256 freeze the13 changed package files for Dewey. All three handoff
deviations and Mos1510's two preservation conditions are explicit. Old-test numstats remain80/66 and3/1.
No full webui/npm ci/build was run; Sage explicitly reserved it for a later window. Full package/script
and dependency integration gate remains required before landing. No queue move, merge or live action.
-67
View File
@@ -1,67 +0,0 @@
# Row 225 review packet — C2-10b (#1726)
Owner Fethry (14f80e3c), reviewer Dewey (84fb346c). Base 45dca69f6.
Source candidate 507901277 continues Morgana 66f7becf2 and 9a131e956; no rewritten history.
Status: targeted tests and full library green; full webui/build and Sage's full gate still pending.
This packet requests source/manifest review, not landing or a full-green claim.
## Behavior and contracts
The Console shows reply text and citations with path/line/quote, scope and agent-written labels,
marks a changed file only with valid differing check.file/check.last, links only https URLs, shows
write/delete notices and explicit Revert results, and displays library status, fetch age, divergence,
fetch errors, nested replyLimits metadata and R-problems. Invalid library mappings remain visible.
Revert validates {project,commit}, preserves refusal codes and reports uncertain/wrong-project
receipts as outcome-unknown without retry. The project is chat project.repo (business.projects key).
A1-A5 are in BUILD.md. Dependency contracts: row217 ordered check.citations and nested replyLimits;
row288 A2 revert result/notice. No dependency merge in this candidate. Landing order is 217 then225;
unknown-verb is expected until288 lands. Beakley's GUARD_CODES/FIXED_TEXTS need no renderer import:
renderReply defines neither and renders those fixed bodies as free text.
## Frozen implementation and tests
manifest.r1.sha256 freezes the 13 changed package paths, including the two approved existing-test
changes. Check from repository root with `sha256sum -c agents/fethry/work/queue-225/manifest.r1.sha256`.
All other changed files are BUILD/session records, handoff/approval evidence and this review packet.
No role, policy, schema, secret, live instance, deployment or queue change is included.
Three departures from the brief, carried from Morgana's handoff:
1. Chat rendering/status live in public/chat.js and chat.css, where the message list is, instead of
bus.js and its stylesheet (sage-3596). serve.mjs serves the same pure render.mjs module to owner
and granted-user pages; no duplicated browser renderer and no generated asset.
2. console-bus.mjs and cli.mjs wire libraryRevert beside libraryStatus (sage-3596). --send still
independently gates Revert. Preserve row288's separate librarySave wiring at integration.
3. webui/src/bus.mjs maps library-revert-not-agent to409; bus/tests/channel-proof.test.mjs re-pins
only that source's hash and documents why. Mos read and approved this exact +3/-1 (mos-1510).
Existing-test authority: Mos1510 via Sage3623 approves render.test.mjs +80/-66 and channel-proof +3/-1.
Both exact numstats are unchanged. The malformed-body fixed-line rule is superseded by design5.2's
whole-body free text, covered by two replacement renderer tests. The old no-options path-badge rule
is retained and covered by the replacement test plus unchecked-cite browser coverage. BUILD.md
explains these two preservation conditions and the three new-file assertion corrections in detail.
## Verification and remaining gate
Evidence logs are under evidence/; commands, deadlines, failures and corrections are in BUILD.md.
Targeted whole-file tests: renderer9/0, routes13/0, browser9/0. Full library81/0, channel-proof2/0.
No skips/cancellations. Invalid-mapping visibility also has an independent RED then GREEN.
All 14 isolated mutants were killed by assertions on source candidate507901277; each mutation
matched one anchor, restored its source hash, and verified unchanged candidate hashes before the next
mutation. Summary: evidence/mutants-final/summary.json. The first attempt stopped at an expected-state
wait (not counted as a kill), then the new browser test was improved and the whole kit rerun.
Raw logs remain under the scratch root; checked-in display copies trim trailing whitespace and record
raw digests in evidence/raw-log-provenance.json.
Before landing, Sage still runs full webui with MOSAIC_REQUIRE_EDITOR_BUILD=1 after the worktree-local
webui npm ci, every applicable package/script suite and integration against217. No full webui/build
in T1 (sage-3647). Dewey approval and Sage's gate/landing remain required; queue moves belong to Sage.
Static audit: the channel-proof source pin matches webui bus.mjs; no hash pin of bus.js, serve.mjs
or library render.mjs was found. packages' only remaining renderItems match is an explanatory comment
in the frozen replacement test; no runtime export, import or call remains. No no-cited-answer match.
Review focus: stored check metadata versus untrusted body, missing/malformed file/last, https-only
links and inert hostile text, Revert authority/receipt uncertainty, async route/sign-out isolation,
and status diagnostics without owner transport leakage to a granted user.
@@ -1,64 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 704.129155
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 690.249337
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 595.036289
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 627.082447
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 787.799722
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 587.311743
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 618.209773
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 633.838649
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 619.313248
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6014.970062
@@ -1,64 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 636.096036
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 741.239029
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 568.879998
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 641.929005
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 870.041985
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 648.87451
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 566.725897
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 611.150846
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 618.488891
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6014.364529
@@ -1,16 +0,0 @@
✔ chat replies: free text, cites with badges, the agent label, changed since and safe links (583.234335ms)
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (634.71741ms)
✔ chat replies: a read-only Console shows the notice with Revert disabled (574.836149ms)
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (607.74749ms)
✔ chat status line: limits when the status carries them, no problems, and refusals (861.034669ms)
✔ chat status line: a project with no repository link reads no library status (611.433469ms)
✔ chat status line: unreadable reply limits show their problem and both fallback keys (606.380675ms)
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (650.947719ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5251.735356
@@ -1,78 +0,0 @@
✖ chat replies: free text, cites with badges, the agent label, changed since and safe links (980.914799ms)
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (753.988771ms)
✔ chat replies: a read-only Console shows the notice with Revert disabled (672.814814ms)
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (622.859977ms)
✖ chat status line: limits when the status carries them, no problems, and refusals (583.956229ms)
✔ chat status line: a project with no repository link reads no library status (635.758222ms)
✖ chat status line: unreadable reply limits show their problem and both fallback keys (581.636121ms)
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (653.788881ms)
ℹ tests 8
ℹ suites 0
ℹ pass 5
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5597.371605
✖ failing tests:
test at packages/webui/tests/chat-replies-browser.test.mjs:81:1
✖ chat replies: free text, cites with badges, the agent label, changed since and safe links (980.914799ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ undefined
- false
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:102:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: false,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/webui/tests/chat-replies-browser.test.mjs:152:1
✖ chat status line: limits when the status carries them, no problems, and refusals (583.956229ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
- 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:156:10)
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) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems',
expected: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/webui/tests/chat-replies-browser.test.mjs:173:1
✖ chat status line: unreadable reply limits show their problem and both fallback keys (581.636121ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · no problems'
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:179:10)
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) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · no problems',
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems',
operator: 'strictEqual',
diff: 'simple'
}
@@ -1,22 +0,0 @@
TAP version 13
# Subtest: channel conversion preserves the four proof/transport byte baselines and modules cannot import a proof caller
ok 1 - channel conversion preserves the four proof/transport byte baselines and modules cannot import a proof caller
---
duration_ms: 4.280459
type: 'test'
...
# Subtest: channel recovery still requires the same shim path and rejects an absent or forged proof
ok 2 - channel recovery still requires the same shim path and rejects an absent or forged proof
---
duration_ms: 413.032186
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 539.602759
@@ -1,496 +0,0 @@
TAP version 13
# Subtest: resolver step 1: a path from the repo root, with or without .md
ok 1 - resolver step 1: a path from the repo root, with or without .md
---
duration_ms: 1.051208
type: 'test'
...
# Subtest: resolver step 2: the same path with vaultPrefix/ stripped
ok 2 - resolver step 2: the same path with vaultPrefix/ stripped
---
duration_ms: 0.164795
type: 'test'
...
# Subtest: resolver step 3: a path relative to the linking file, never above the repo root
ok 3 - resolver step 3: a path relative to the linking file, never above the repo root
---
duration_ms: 0.192765
type: 'test'
...
# Subtest: resolver step 4: a base name that matches exactly one indexed file
ok 4 - resolver step 4: a base name that matches exactly one indexed file
---
duration_ms: 0.201611
type: 'test'
...
# Subtest: a link into another of her repos resolves to nothing
ok 5 - a link into another of her repos resolves to nothing
---
duration_ms: 0.130282
type: 'test'
...
# Subtest: wikilink forms, embeds, and links skipped inside code
ok 6 - wikilink forms, embeds, and links skipped inside code
---
duration_ms: 1.445011
type: 'test'
...
# Subtest: only a body embed whose target has an extension other than .md, in any case, is an attachment
ok 7 - only a body embed whose target has an extension other than .md, in any case, is an attachment
---
duration_ms: 0.25003
type: 'test'
...
# Subtest: the fixture mapping checks and comes back frozen
ok 8 - the fixture mapping checks and comes back frozen
---
duration_ms: 1.772928
type: 'test'
...
# Subtest: C2-01c: a prefix inside or holding another entry's book refuses library-playground-other-universe
ok 9 - C2-01c: a prefix inside or holding another entry's book refuses library-playground-other-universe
---
duration_ms: 0.735312
type: 'test'
...
# Subtest: C2-01c: a prefix equal to a book folder, or holding its own, refuses library-mapping-invalid
ok 10 - C2-01c: a prefix equal to a book folder, or holding its own, refuses library-mapping-invalid
---
duration_ms: 0.532498
type: 'test'
...
# Subtest: C2-01c: universe-level prefixes lie outside every book, in a plain universe and in a hybrid with no books
ok 11 - C2-01c: universe-level prefixes lie outside every book, in a plain universe and in a hybrid with no books
---
duration_ms: 0.668173
type: 'test'
...
# Subtest: C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []
ok 12 - C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []
---
duration_ms: 1.032507
type: 'test'
...
# Subtest: no playground prefix holds another, and no book folder holds another
ok 13 - no playground prefix holds another, and no book folder holds another
---
duration_ms: 0.507943
type: 'test'
...
# Subtest: shape refusals: kind, remote, branch, unknown keys, types, manuscript, folder names
ok 14 - shape refusals: kind, remote, branch, unknown keys, types, manuscript, folder names
---
duration_ms: 0.740368
type: 'test'
...
# Subtest: C2-03a sage-2021: user is optional, kept when given, and must be a plain account name
ok 15 - C2-03a sage-2021: user is optional, kept when given, and must be a plain account name
---
duration_ms: 0.659277
type: 'test'
...
# Subtest: a hybrid universe is a shape the query code can take (1.4), with sources that must exist
ok 16 - a hybrid universe is a shape the query code can take (1.4), with sources that must exist
---
duration_ms: 0.695117
type: 'test'
...
# Subtest: a caller-supplied fail receives the code, as business file loading will pass it (C2-04)
ok 17 - a caller-supplied fail receives the code, as business file loading will pass it (C2-04)
---
duration_ms: 0.392362
type: 'test'
...
# Subtest: scope comes from the path alone (design 1.2)
ok 18 - scope comes from the path alone (design 1.2)
---
duration_ms: 0.466406
type: 'test'
...
# Subtest: C2-04 mirror: exact default path, bare without checkout or remote, and idempotent initialization
ok 19 - C2-04 mirror: exact default path, bare without checkout or remote, and idempotent initialization
---
duration_ms: 46.645994
type: 'test'
...
# Subtest: C2-04 mirror: simultaneous creators publish one intact mirror and leave no staging paths
ok 20 - C2-04 mirror: simultaneous creators publish one intact mirror and leave no staging paths
---
duration_ms: 32.763959
type: 'test'
...
# Subtest: C2-04 mirror: skipped publication accepts both coreutils statuses without removing the winner
ok 21 - C2-04 mirror: skipped publication accepts both coreutils statuses without removing the winner
---
duration_ms: 109.751094
type: 'test'
...
# Subtest: C2-04 mirror: preserve occupied directories and reject symlink ancestors and code overlaps
ok 22 - C2-04 mirror: preserve occupied directories and reject symlink ancestors and code overlaps
---
duration_ms: 20.129293
type: 'test'
...
# Subtest: C2-04 mirror: existing metadata accepts only bare Git plus state and the precreated lock
ok 23 - C2-04 mirror: existing metadata accepts only bare Git plus state and the precreated lock
---
duration_ms: 24.021555
type: 'test'
...
# Subtest: C2-04 state: missing state is empty only while the served ref is absent
ok 24 - C2-04 state: missing state is empty only while the served ref is absent
---
duration_ms: 37.436349
type: 'test'
...
# Subtest: C2-04 state: valid problems and sync metadata return no content and change no state bytes
ok 25 - C2-04 state: valid problems and sync metadata return no content and change no state bytes
---
duration_ms: 37.681315
type: 'test'
...
# Subtest: C2-04 state: strict keys, types, times, branch, commit and fixed R messages fail closed
ok 26 - C2-04 state: strict keys, types, times, branch, commit and fixed R messages fail closed
---
duration_ms: 105.483341
type: 'test'
...
# Subtest: C2-04 state: symlink, hardlink, non-private state and missing lock refuse without replacement
ok 27 - C2-04 state: symlink, hardlink, non-private state and missing lock refuse without replacement
---
duration_ms: 29.562786
type: 'test'
...
# Subtest: C2-04 file: served commit, not HEAD or a caller commit, owns the exact returned text
ok 28 - C2-04 file: served commit, not HEAD or a caller commit, owns the exact returned text
---
duration_ms: 74.057232
type: 'test'
...
# Subtest: C2-04 file: R1, R2 and R7 exclusions and never-synced mirrors cannot return file bodies
ok 29 - C2-04 file: R1, R2 and R7 exclusions and never-synced mirrors cannot return file bodies
---
duration_ms: 80.573844
type: 'test'
...
# Subtest: C2-04 lock: a first-sync and fast-forward reader wait for a consistent pair across processes
ok 30 - C2-04 lock: a first-sync and fast-forward reader wait for a consistent pair across processes
---
duration_ms: 253.247948
type: 'test'
...
# Subtest: C2-04 lock: same-process waits, timeout code, callback expiry and throw release
ok 31 - C2-04 lock: same-process waits, timeout code, callback expiry and throw release
---
duration_ms: 5031.462795
type: 'test'
...
# Subtest: C2-04 lock: owner death releases the kernel lock without replacing its file
ok 32 - C2-04 lock: owner death releases the kernel lock without replacing its file
---
duration_ms: 61.533295
type: 'test'
...
# Subtest: C2-04 lock: a missing flock binary becomes a handled refusal
ok 33 - C2-04 lock: a missing flock binary becomes a handled refusal
---
duration_ms: 57.176644
type: 'test'
...
# Subtest: C2-04 lock: an unawaited scoped read drains before the lock is released
ok 34 - C2-04 lock: an unawaited scoped read drains before the lock is released
---
duration_ms: 22.766874
type: 'test'
...
# Subtest: the fixture reads clean: every resolver step, appears_in and relationships wikilinks indexed
ok 35 - the fixture reads clean: every resolver step, appears_in and relationships wikilinks indexed
---
duration_ms: 33.888061
type: 'test'
...
# Subtest: R1: executables, symlinks, gitlinks, blobs over 1 MiB and non-UTF-8 text are not indexed
ok 36 - R1: executables, symlinks, gitlinks, blobs over 1 MiB and non-UTF-8 text are not indexed
---
duration_ms: 68.951498
type: 'test'
...
# Subtest: R2: unmapped paths are reported and not indexed
ok 37 - R2: unmapped paths are reported and not indexed
---
duration_ms: 25.70771
type: 'test'
...
# Subtest: R3: front matter not fully parsed stays raw, with the line where parsing stopped
ok 38 - R3: front matter not fully parsed stays raw, with the line where parsing stopped
---
duration_ms: 26.059427
type: 'test'
...
# Subtest: front matter forms the reader accepts and refuses
ok 39 - front matter forms the reader accepts and refuses
---
duration_ms: 0.448293
type: 'test'
...
# Subtest: R4: a link to no file, to more than one, or into another repo is reported and not followed
ok 40 - R4: a link to no file, to more than one, or into another repo is reported and not followed
---
duration_ms: 26.225692
type: 'test'
...
# Subtest: a block list link carries its item line, a scalar or flow link its key line, and problems follow
ok 41 - a block list link carries its item line, a scalar or flow link its key line, and problems follow
---
duration_ms: 22.565103
type: 'test'
...
# Subtest: an unresolved attachment embed gives no problem; a link or front matter value to one is R4; a note embed resolves
ok 42 - an unresolved attachment embed gives no problem; a link or front matter value to one is R4; a note embed resolves
---
duration_ms: 20.903777
type: 'test'
...
# Subtest: an indexed Dr.Quinn.md resolves as a body link, a block list link and an embed
ok 43 - an indexed Dr.Quinn.md resolves as a body link, a block list link and an embed
---
duration_ms: 25.42747
type: 'test'
...
# Subtest: R5: a canon file that refers into the playground is reported, and the link still goes by path
ok 44 - R5: a canon file that refers into the playground is reported, and the link still goes by path
---
duration_ms: 27.37119
type: 'test'
...
# Subtest: R6: a canon file that refers into another universe is reported; a hybrid may refer into its sources
ok 45 - R6: a canon file that refers into another universe is reported; a hybrid may refer into its sources
---
duration_ms: 30.585458
type: 'test'
...
# Subtest: R7: two paths that differ only in case are both left out
ok 46 - R7: two paths that differ only in case are both left out
---
duration_ms: 40.709509
type: 'test'
...
# Subtest: the reader reads the commit, never the working tree, and refuses a bad commit id
ok 47 - the reader reads the commit, never the working tree, and refuses a bad commit id
---
duration_ms: 33.579454
type: 'test'
...
# Subtest: a bare mirror reads the same as the clone
ok 48 - a bare mirror reads the same as the clone
---
duration_ms: 38.368686
type: 'test'
...
# Subtest: free text renders as itself with no citations
ok 49 - free text renders as itself with no citations
---
duration_ms: 1.571777
type: 'test'
...
# Subtest: a cited reply renders its text, then each cite with the badge from the check result
ok 50 - a cited reply renders its text, then each cite with the badge from the check result
---
duration_ms: 0.537429
type: 'test'
...
# Subtest: a parsed body renders as its string does
ok 51 - a parsed body renders as its string does
---
duration_ms: 0.304068
type: 'test'
...
# Subtest: without a check result a path cite has no badge, no label and is never changed
ok 52 - without a check result a path cite has no badge, no label and is never changed
---
duration_ms: 0.184363
type: 'test'
...
# Subtest: the label and the badge come from the check result only, never from a bad entry
ok 53 - the label and the badge come from the check result only, never from a bad entry
---
duration_ms: 0.180581
type: 'test'
...
# Subtest: a web cite is a link only for https
ok 54 - a web cite is a link only for https
---
duration_ms: 0.293669
type: 'test'
...
# Subtest: a write notice renders its fixed line
ok 55 - a write notice renders its fixed line
---
duration_ms: 0.277197
type: 'test'
...
# Subtest: a body without the reply or notice shape is free text: the whole body, no citations
ok 56 - a body without the reply or notice shape is free text: the whole body, no citations
---
duration_ms: 0.345807
type: 'test'
...
# Subtest: an empty citations list is a reply with no cites
ok 57 - an empty citations list is a reply with no cites
---
duration_ms: 0.187354
type: 'test'
...
# Subtest: C2-01c: each level gives its universe, and its book or null
ok 58 - C2-01c: each level gives its universe, and its book or null
---
duration_ms: 3.363132
type: 'test'
...
# Subtest: C2-01c: prefixes come first: in a book that holds a prefix, a file outside it is canon and one under it playground
ok 59 - C2-01c: prefixes come first: in a book that holds a prefix, a file outside it is canon and one under it playground
---
duration_ms: 0.480483
type: 'test'
...
# Subtest: C2-01c: a canon file linking into a draft gives R6, and into its own universe-level prefix gives R5
ok 60 - C2-01c: a canon file linking into a draft gives R6, and into its own universe-level prefix gives R5
---
duration_ms: 44.606565
type: 'test'
...
# Subtest: the survey prints folder names, counts, key sets and link-form counts, and never text
ok 61 - the survey prints folder names, counts, key sets and link-form counts, and never text
---
duration_ms: 37.28555
type: 'test'
...
# Subtest: folder names print DEL, C1 and bidi controls, U+061C among them, as escapes
ok 62 - folder names print DEL, C1 and bidi controls, U+061C among them, as escapes
---
duration_ms: 26.222951
type: 'test'
...
# Subtest: an embed of an attachment counts as an embed and as an attachment; a plain link to one does not
ok 63 - an embed of an attachment counts as an embed and as an attachment; a plain link to one does not
---
duration_ms: 26.23793
type: 'test'
...
# Subtest: with --mapping the survey adds scope, resolver step and problem counts
ok 64 - with --mapping the survey adds scope, resolver step and problem counts
---
duration_ms: 41.437066
type: 'test'
...
# Subtest: survey exit codes: usage 4, bad mapping 3, unreadable repo 2
ok 65 - survey exit codes: usage 4, bad mapping 3, unreadable repo 2
---
duration_ms: 17.463059
type: 'test'
...
# Subtest: scripts/mosaic library survey reaches the survey
ok 66 - scripts/mosaic library survey reaches the survey
---
duration_ms: 77.634947
type: 'test'
...
# Subtest: C2-03a argv: the fixed sequence, one https allow, no other protocol key
ok 67 - C2-03a argv: the fixed sequence, one https allow, no other protocol key
---
duration_ms: 1.605597
type: 'test'
...
# Subtest: C2-03a argv: only an https URL with a plain host, no userinfo, and a plain user
ok 68 - C2-03a argv: only an https URL with a plain host, no userinfo, and a plain user
---
duration_ms: 0.713147
type: 'test'
...
# Subtest: C2-03a helper: answers only the exact prompts for the configured host and user (dewey-0765)
ok 69 - C2-03a helper: answers only the exact prompts for the configured host and user (dewey-0765)
---
duration_ms: 31.590474
type: 'test'
...
# Subtest: C2-03a first sync: served is the fetched tip, HEAD is served, the survey reads it, and the token stays in the slot
ok 70 - C2-03a first sync: served is the fetched tip, HEAD is served, the survey reads it, and the token stays in the slot
---
duration_ms: 135.075159
type: 'test'
...
# Subtest: C2-03a fast-forward: served moves, problems are rebuilt, and a repeat changes only lastFetch
ok 71 - C2-03a fast-forward: served moves, problems are rebuilt, and a repeat changes only lastFetch
---
duration_ms: 225.707972
type: 'test'
...
# Subtest: C2-03a Filbert N2: a read that took served before a sync still finds its objects after served moves
ok 72 - C2-03a Filbert N2: a read that took served before a sync still finds its objects after served moves
---
duration_ms: 205.10431
type: 'test'
...
# Subtest: C2-03a overlap: a reader during a sync sees the old pair or the new one, never a mix
ok 73 - C2-03a overlap: a reader during a sync sees the old pair or the new one, never a mix
---
duration_ms: 175.907929
type: 'test'
...
# Subtest: C2-03a diverged: a non-fast-forward keeps serving and holds until a human accepts the tip
ok 74 - C2-03a diverged: a non-fast-forward keeps serving and holds until a human accepts the tip
---
duration_ms: 431.553812
type: 'test'
...
# Subtest: C2-03a accept starts the record over, so a changed branch gets a fresh state
ok 75 - C2-03a accept starts the record over, so a changed branch gets a fresh state
---
duration_ms: 195.011432
type: 'test'
...
# Subtest: C2-03a fetch failure: a wrong token keeps serving and records the error, lastFetch unchanged
ok 76 - C2-03a fetch failure: a wrong token keeps serving and records the error, lastFetch unchanged
---
duration_ms: 192.898644
type: 'test'
...
# Subtest: C2-03a a failed first fetch records the error with nothing served
ok 77 - C2-03a a failed first fetch records the error with nothing served
---
duration_ms: 61.594695
type: 'test'
...
# Subtest: C2-03a a 302 to another host fails the fetch and the other host sees nothing
ok 78 - C2-03a a 302 to another host fails the fetch and the other host sees nothing
---
duration_ms: 64.82991
type: 'test'
...
# Subtest: C2-03a the slot: missing or unsafe refuses before git, and sync never opens it
ok 79 - C2-03a the slot: missing or unsafe refuses before git, and sync never opens it
---
duration_ms: 59.167725
type: 'test'
...
# Subtest: C2-03a sage-2021: an entry with no user refuses library-remote-invalid and runs no git
ok 80 - C2-03a sage-2021: an entry with no user refuses library-remote-invalid and runs no git
---
duration_ms: 35.37205
type: 'test'
...
# Subtest: C2-03a one sync per mirror: an overlapping call refuses library-sync-busy
ok 81 - C2-03a one sync per mirror: an overlapping call refuses library-sync-busy
---
duration_ms: 118.260687
type: 'test'
...
1..81
# tests 81
# suites 0
# pass 81
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 9149.325891
@@ -1,48 +0,0 @@
✔ chat replies: free text, cites with badges, the agent label, changed since and safe links (633.807074ms)
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (621.95095ms)
✔ chat replies: a read-only Console shows the notice with Revert disabled (577.638161ms)
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (555.847899ms)
✖ chat status line: limits when the status carries them, no problems, and refusals (3725.696272ms)
✔ chat status line: a project with no repository link reads no library status (657.572856ms)
✔ chat status line: unreadable reply limits show their problem and both fallback keys (663.163445ms)
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (616.184063ms)
✖ chat status line: an invalid library mapping remains visible for diagnosis (597.976623ms)
ℹ tests 9
ℹ suites 0
ℹ pass 7
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8782.679569
✖ failing tests:
test at packages/webui/tests/chat-replies-browser.test.mjs:152:1
✖ chat status line: limits when the status carries them, no problems, and refusals (3725.696272ms)
Error: Uncaught (in promise) Error: window.__statusReads.length && document.querySelector(".chat-conversation") && document.querySelector(".chat-library")?.textContent==="Library status unavailable (library-mapping-invalid).": undefined
at Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/browser.mjs:49:35)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:162:5)
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/webui/tests/chat-replies-browser.test.mjs:200:1
✖ chat status line: an invalid library mapping remains visible for diagnosis (597.976623ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ null
- 'Library status unavailable (library-mapping-invalid).'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:203:10)
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) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: null,
expected: 'Library status unavailable (library-mapping-invalid).',
operator: 'strictEqual',
diff: 'simple'
}
@@ -1,4 +0,0 @@
{"mutant": "M1-badges", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-badges.tap"}
{"mutant": "M1-agent-label", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-agent-label.tap"}
{"mutant": "M2-http-links", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M2-http-links.tap"}
{"mutant": "M3-wrong-commit", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": false, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M3-wrong-commit.tap"}
@@ -1,192 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 627.180675
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
badge: 'canon',
changed: false,
...
{
+ agent: null,
- agent: 'agent-written: acme/scribe',
badge: 'supporting',
changed: false,
href: null,
quote: 'green eyes',
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: ~
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 778.572201
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 641.728604
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 629.327348
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 837.886073
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 613.037186
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 651.026507
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 735.546636
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 712.174542
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6353.090574
@@ -1,206 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 638.589537
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
+ badge: null,
- badge: 'canon',
changed: false,
href: null,
quote: 'her grey eyes',
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa',
rel: null
...
agent: null,
+ badge: null,
- badge: 'playground',
changed: true,
href: null,
quote: 'a storm',
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa',
rel: null
...
agent: 'agent-written: acme/scribe',
+ badge: null,
- badge: 'supporting',
changed: false,
href: null,
quote: 'green eyes',
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
rel: null
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: ~
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: ~
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: ~
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 770.070531
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 519.328288
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 647.667301
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 999.237447
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 676.088845
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 634.472637
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 655.963182
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 578.819646
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6239.022812
@@ -1,204 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 655.901619
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
badge: 'canon',
changed: false,
...
changed: false,
+ href: 'http://example.invalid/plain',
- href: null,
quote: 'plain http',
ref: 'http://example.invalid/plain',
+ rel: 'noopener noreferrer'
- rel: null
},
{
agent: null,
badge: 'web',
changed: false,
+ href: 'javascript:window.injected=2',
- href: null,
quote: 'script',
ref: 'javascript:window.injected=2',
+ rel: 'noopener noreferrer'
- rel: null
}
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: 'http://example.invalid/plain'
rel: 'noopener noreferrer'
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: 'javascript:window.injected=2'
rel: 'noopener noreferrer'
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 741.122544
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 616.728572
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 625.818493
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 867.033773
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 655.587703
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 615.997516
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 648.263681
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 624.323397
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6168.633364
@@ -1,74 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 640.32042
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 694.233373
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 611.500508
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 606.581119
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 896.462474
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 666.798177
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 629.105559
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 3654.057789
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:184:1'
failureType: 'testCodeFailure'
error: 'Uncaught (in promise) Error: document.querySelectorAll(".chat-changed").length===0: undefined'
code: 'ERR_TEST_FAILURE'
stack: |-
Object.evaluate (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/browser.mjs:49:35)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:195:3)
async Test.run (node:internal/test_runner/test:1409:7)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 656.44095
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 9179.328916
@@ -1,47 +0,0 @@
{
"head": "77172ce3b46eb289409068518b7022fc7be47ebe",
"clear": "sage-3647",
"worktreeUnchanged": true,
"results": [
{
"mutant": "M1-badges",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-badges.tap"
},
{
"mutant": "M1-agent-label",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-agent-label.tap"
},
{
"mutant": "M2-http-links",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M2-http-links.tap"
},
{
"mutant": "M3-wrong-commit",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": false,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M3-wrong-commit.tap"
}
]
}
@@ -1,14 +0,0 @@
{"mutant": "M1-badges", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-badges.tap"}
{"mutant": "M1-agent-label", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-agent-label.tap"}
{"mutant": "M2-http-links", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M2-http-links.tap"}
{"mutant": "M3-wrong-commit", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-wrong-commit.tap"}
{"mutant": "M3-missing-file", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-missing-file.tap"}
{"mutant": "M4-fallback", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-fallback.tap"}
{"mutant": "M4-problem", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-problem.tap"}
{"mutant": "M5-send", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-send.tap"}
{"mutant": "M5-project-receipt", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-project-receipt.tap"}
{"mutant": "M5-commit-input", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-commit-input.tap"}
{"mutant": "M5-origin", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-origin.tap"}
{"mutant": "M5-fetch-metadata", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-fetch-metadata.tap"}
{"mutant": "M6-duplicate-call", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M6-duplicate-call.tap"}
{"mutant": "M7-user-fence", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M7-user-fence.tap"}
@@ -1,192 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 646.851404
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
badge: 'canon',
changed: false,
...
{
+ agent: null,
- agent: 'agent-written: acme/scribe',
badge: 'supporting',
changed: false,
href: null,
quote: 'green eyes',
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: ~
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 667.90945
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 588.346681
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 485.882289
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 709.847041
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 544.885551
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 514.410016
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 542.746154
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 582.608663
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5409.980177
@@ -1,206 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 621.562133
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
+ badge: null,
- badge: 'canon',
changed: false,
href: null,
quote: 'her grey eyes',
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa',
rel: null
...
agent: null,
+ badge: null,
- badge: 'playground',
changed: true,
href: null,
quote: 'a storm',
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa',
rel: null
...
agent: 'agent-written: acme/scribe',
+ badge: null,
- badge: 'supporting',
changed: false,
href: null,
quote: 'green eyes',
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
rel: null
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: ~
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: ~
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: ~
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 731.101767
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 633.163696
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 637.761161
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 847.929653
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 635.512979
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 645.427688
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 672.272209
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 629.043251
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6169.954716
@@ -1,204 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 582.248796
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
{
agent: null,
badge: 'canon',
changed: false,
...
changed: false,
+ href: 'http://example.invalid/plain',
- href: null,
quote: 'plain http',
ref: 'http://example.invalid/plain',
+ rel: 'noopener noreferrer'
- rel: null
},
{
agent: null,
badge: 'web',
changed: false,
+ href: 'javascript:window.injected=2',
- href: null,
quote: 'script',
ref: 'javascript:window.injected=2',
+ rel: 'noopener noreferrer'
- rel: null
}
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: ~
rel: ~
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: ~
rel: ~
quote: 'script'
agent: ~
changed: false
actual:
0:
badge: 'canon'
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
href: ~
rel: ~
quote: 'her grey eyes'
agent: ~
changed: false
1:
badge: 'playground'
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
href: ~
rel: ~
quote: 'a storm'
agent: ~
changed: true
2:
badge: 'supporting'
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
href: ~
rel: ~
quote: 'green eyes'
agent: 'agent-written: acme/scribe'
changed: false
3:
badge: 'web'
ref: 'https://example.invalid/tides'
href: 'https://example.invalid/tides'
rel: 'noopener noreferrer'
quote: 'spring tides'
agent: ~
changed: false
4:
badge: 'web'
ref: 'http://example.invalid/plain'
href: 'http://example.invalid/plain'
rel: 'noopener noreferrer'
quote: 'plain http'
agent: ~
changed: false
5:
badge: 'web'
ref: 'javascript:window.injected=2'
href: 'javascript:window.injected=2'
rel: 'noopener noreferrer'
quote: 'script'
agent: ~
changed: false
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:90: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: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 640.769563
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 572.506418
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 519.031091
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 743.513043
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 676.282756
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 595.166769
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 626.475981
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 596.828198
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5658.889854
@@ -1,81 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 639.027823
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 693.772757
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 683.094789
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 637.029355
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 772.100011
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 599.342035
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 574.320042
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 621.466015
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-missing-file/packages/webui/tests/chat-replies-browser.test.mjs:184: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/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-missing-file/packages/webui/tests/chat-replies-browser.test.mjs:196: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: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 590.165121
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5921.651346
@@ -1,81 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 704.141588
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 816.229452
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 697.826745
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 638.579753
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 850.914117
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 634.004327
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 585.469632
type: 'test'
...
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 673.386968
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:184: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/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:196: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: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 579.53998
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6292.579267
@@ -1,102 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 683.334532
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 686.685624
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 612.757087
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 584.651483
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
not ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 637.405759
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:152:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
- 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
actual: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:156: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: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 626.790802
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
not ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 643.680732
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:173:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · reply-limits-unreadable · no problems'
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · reply-limits-unreadable · no problems'
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:179: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: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 645.032205
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 616.242947
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 7
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5845.226677
@@ -1,83 +0,0 @@
TAP version 13
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
---
duration_ms: 663.005514
type: 'test'
...
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
---
duration_ms: 662.360161
type: 'test'
...
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
---
duration_ms: 620.68694
type: 'test'
...
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
---
duration_ms: 744.825035
type: 'test'
...
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
---
duration_ms: 826.418848
type: 'test'
...
# Subtest: chat status line: a project with no repository link reads no library status
ok 6 - chat status line: a project with no repository link reads no library status
---
duration_ms: 549.365993
type: 'test'
...
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
not ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
---
duration_ms: 589.776762
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-problem/packages/webui/tests/chat-replies-browser.test.mjs:173:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
+ actual - expected
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · no problems'
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · no problems'
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-problem/packages/webui/tests/chat-replies-browser.test.mjs:179: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: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
---
duration_ms: 590.919434
type: 'test'
...
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
---
duration_ms: 545.326381
type: 'test'
...
1..9
# tests 9
# suites 0
# pass 8
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5914.325706
@@ -1,105 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 16.128237
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 5.987607
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
not ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 4.805637
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-commit-input/packages/webui/tests/library-routes.test.mjs:64:1'
failureType: 'testCodeFailure'
error: |-
{"project":"books","commit":"abc"}
200 !== 400
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 400
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-commit-input/packages/webui/tests/library-routes.test.mjs:69: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: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.383811
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 4.000988
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.487122
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 4.9292
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.266317
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.443059
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.048018
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 0.864934
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.064626
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 0.803048
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 12
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 153.3549
@@ -1,105 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 20.271342
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
not ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 5.700968
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-fetch-metadata/packages/webui/tests/library-routes.test.mjs:48:1'
failureType: 'testCodeFailure'
error: |-
sec-fetch-site
200 !== 403
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 403
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-fetch-metadata/packages/webui/tests/library-routes.test.mjs:57: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: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 6.427935
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 7.39929
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 5.425467
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.634037
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 5.727409
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.317954
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.583799
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.207375
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 0.956772
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.147745
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 0.819337
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 12
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 166.134058
@@ -1,105 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 16.249491
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
not ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 5.463985
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-origin/packages/webui/tests/library-routes.test.mjs:48:1'
failureType: 'testCodeFailure'
error: |-
origin
200 !== 403
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 403
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-origin/packages/webui/tests/library-routes.test.mjs:57: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: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 6.035196
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 5.621744
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 4.928333
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.455282
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 4.838747
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.235209
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.436399
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.100574
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 0.943474
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.176957
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 0.861834
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 12
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 150.969641
@@ -1,105 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 18.918199
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 7.26916
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 5.352342
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.134713
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 4.288914
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.745385
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 5.739155
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.264793
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.471965
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.045204
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
not ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 1.531252
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-project-receipt/packages/webui/tests/library-routes.test.mjs:145: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/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-project-receipt/packages/webui/tests/library-routes.test.mjs:148: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: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.60161
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 0.816068
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 12
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 171.289003
@@ -1,106 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 18.930067
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 7.470645
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 6.646929
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.637697
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 4.174441
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.513131
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 5.145692
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.182362
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.471806
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.064814
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 0.910004
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.16191
type: 'test'
...
# 2026-10-23T19:28:51.488Z library-revert user="jason" project="books" commit="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" commit=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# Subtest: a configured revert callback remains unreachable when send is off
not ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 1.624909
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-send/packages/webui/tests/library-routes.test.mjs:175:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
200 !== 403
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 403
actual: 200
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-send/packages/webui/tests/library-routes.test.mjs:181: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..13
# tests 13
# suites 0
# pass 12
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 165.919088
@@ -1,177 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
not ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 17.922489
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:40:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
{
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
project: 'books'
},
+ {
+ commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ project: 'books'
+ }
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
actual:
0:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
1:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:43: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: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 7.053366
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 5.869486
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.852952
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
not ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 2.870452
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:88: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/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:96: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: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.904694
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 5.7962
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.518332
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.563294
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.17446
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
not ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 1.258633
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:145:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
{
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
project: 'books'
},
+ {
+ commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ project: 'books'
+ }
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
actual:
0:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
1:
project: 'books'
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:151: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: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.362877
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 0.848309
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 10
# fail 3
# cancelled 0
# skipped 0
# todo 0
# duration_ms 169.162917
@@ -1,126 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 18.215173
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 6.416847
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 5.529629
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.92431
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 4.588509
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.817257
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 6.020444
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 1.377224
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
not ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 1.015984
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:132:1'
failureType: 'testCodeFailure'
error: |-
/api/library/status
true !== false
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: false
actual: true
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:133:106)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.processPendingSubtests (node:internal/test_runner/test:974:18)
Test.postRun (node:internal/test_runner/test:1542:19)
Test.run (node:internal/test_runner/test:1467:12)
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.695555
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 1.378325
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
not ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.178045
type: 'test'
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:155:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
503 !== 403
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 403
actual: 503
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:167: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: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 1.057394
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 11
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 176.864112
@@ -1,147 +0,0 @@
{
"head": "50790127716002f97154800ecbfb968690a9d644",
"clear": "sage-3647",
"worktreeUnchanged": true,
"results": [
{
"mutant": "M1-badges",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-badges.tap"
},
{
"mutant": "M1-agent-label",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-agent-label.tap"
},
{
"mutant": "M2-http-links",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M2-http-links.tap"
},
{
"mutant": "M3-wrong-commit",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-wrong-commit.tap"
},
{
"mutant": "M3-missing-file",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-missing-file.tap"
},
{
"mutant": "M4-fallback",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-fallback.tap"
},
{
"mutant": "M4-problem",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-problem.tap"
},
{
"mutant": "M5-send",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-send.tap"
},
{
"mutant": "M5-project-receipt",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-project-receipt.tap"
},
{
"mutant": "M5-commit-input",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-commit-input.tap"
},
{
"mutant": "M5-origin",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-origin.tap"
},
{
"mutant": "M5-fetch-metadata",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-fetch-metadata.tap"
},
{
"mutant": "M6-duplicate-call",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M6-duplicate-call.tap"
},
{
"mutant": "M7-user-fence",
"anchorHits": 1,
"restored": true,
"worktreeUnchanged": true,
"exit": 1,
"killed": true,
"test": "packages/webui/tests/library-routes.test.mjs",
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M7-user-fence.tap"
}
]
}
@@ -1,155 +0,0 @@
{
"rawRoot": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2",
"logs": [
{
"path": "evidence/browser-final.tap",
"rawSha256": "4e6d5874786aa5d407b59d0b701652d419d0fecda5402ede40f52b313def12b0",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/browser-final2.tap",
"rawSha256": "194b3150e59868aab023d9ccfc75c69b82e43afba38c1b12e87a1fb3d68fe821",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/browser-green.tap",
"rawSha256": "030d7a5c9af70d4a7ade177906fb971b29fa4cf1fc5efd39f35643ed1969cd01",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/browser-red.tap",
"rawSha256": "db00bee89b4353efe7d324b17417347b30964f79ad4469a5dc98c6d125691f55",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/channel-proof.tap",
"rawSha256": "cb7c0398af2a2a0e7cae24ee2c8167601cbf7e3f9cf3ed7479b2a6ef65f5da8a",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/library-full.tap",
"rawSha256": "5447e3fce497a0dfb03ddd3eb2795dc2a179554f794e4c37458219c70736a645",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mapping-red.tap",
"rawSha256": "8ca7310e46df2d35fa96e3aba378721ad388dce41f003ee133b684eeb7eaf245",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-attempt1/M1-agent-label.tap",
"rawSha256": "db7ab1c64bc05b719e5164ed4ce1dd8b79ad53309d9b8e3e1ca0cc8867bca70f",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-attempt1/M1-badges.tap",
"rawSha256": "08351b9061351812008ebd7067a9aed24a9e5386d8f6284fb44ae0f59c8f2eca",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-attempt1/M2-http-links.tap",
"rawSha256": "69921707c219d19fb2c33a3995268b6442d783ec702050b32f78d8d2db6fb581",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-attempt1/M3-wrong-commit.tap",
"rawSha256": "fe9fb027efae26799e2dfac539e0986402cefe806ca31479f52420e67a8df423",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M1-agent-label.tap",
"rawSha256": "afbab60cbc12348c4d0c4be1a8c4590dd157c71f9ffc7375bbde82facb9868bb",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M1-badges.tap",
"rawSha256": "3bb9fb9dcf74a398934d6de48c2ca99ab2eab618fb944297b3d7db00eec0e69f",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M2-http-links.tap",
"rawSha256": "8909921fcf602fcafa94edd19c1167a8f91ed970a3e649110e3503836690e567",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M3-missing-file.tap",
"rawSha256": "3fd2749ea5bc4245f7d49f585b82eae60f61ed150ab4dd22e8148a1e9467c8c7",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M3-wrong-commit.tap",
"rawSha256": "85696c155f0a2844ee903c8fb38cab0a0fb6b0610c8b9ee8aa77a08cb6012ca2",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M4-fallback.tap",
"rawSha256": "6f59f2845e0130d4d2fe1cb4aa7a435502f6043e11ed4efd67339c12e986103a",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M4-problem.tap",
"rawSha256": "61ab08b823eb08cbb9c9eeaac82b297ea5d1272bb5dc64e9598734688b55eb80",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M5-commit-input.tap",
"rawSha256": "b703ee5ca47e5c64749aab3edfb5ae2e085367921ffa75337f4f0e5c7b3dc404",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M5-fetch-metadata.tap",
"rawSha256": "6951725340e4d74764552e7e406e0fc4bafeec35172de56d13bca852b92daa1a",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M5-origin.tap",
"rawSha256": "95fcb5a92e82fa7de2972fd8f78c4f0f6d1c5f49885123e01fb1b69c3b62ba21",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M5-project-receipt.tap",
"rawSha256": "a86806a189ef73f61ddd88479336321964eadef0793c7b45da6573ccd3e38801",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M5-send.tap",
"rawSha256": "4cce523356a3e2041197156412eb35088f8b6d38fcceca2d9b76b6098ad17627",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M6-duplicate-call.tap",
"rawSha256": "6dda962389fbc7379ac7e7bc79cc24b68db00d4f9bb649ab73e75eaa87a5da00",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/mutants-final/M7-user-fence.tap",
"rawSha256": "abec36c65c3ece6e7d0c4dfd41eede62c9a585f07b3ed4cb3b8d05f0dbabaed3",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/render-green.tap",
"rawSha256": "98463ac3d1262f84758def8bbdc27c46fa7c674bb8e8c20b9c752083163d0864",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/render-red.tap",
"rawSha256": "03645c7a5ba0c3d285e58e1d6fd05dc58a1174b463a258b01bcf258b2141a85e",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/routes-final.tap",
"rawSha256": "f2b58a97c301dc9e57458d82785bd50debb2254df5d411d8e8c4e6b9c0aa00d2",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/routes-green.tap",
"rawSha256": "c34451a0eef5f82fe6ddcbbf370179d8e8a14e36168175bdeff5b32e2072e9d6",
"normalization": "trailing whitespace removed from display copy"
},
{
"path": "evidence/routes-red.tap",
"rawSha256": "5ca5bafe2465209128508e5459f4826c34d00de86d1e79671196dc4e5c57f874",
"normalization": "trailing whitespace removed from display copy"
}
]
}
@@ -1,17 +0,0 @@
✔ free text renders as itself with no citations (1.332106ms)
✔ a cited reply renders its text, then each cite with the badge from the check result (0.493062ms)
✔ a parsed body renders as its string does (0.2706ms)
✔ without a check result a path cite has no badge, no label and is never changed (0.175324ms)
✔ the label and the badge come from the check result only, never from a bad entry (0.171275ms)
✔ a web cite is a link only for https (0.254363ms)
✔ a write notice renders its fixed line (0.201038ms)
✔ a body without the reply or notice shape is free text: the whole body, no citations (0.315846ms)
✔ an empty citations list is a reply with no cites (0.167552ms)
ℹ tests 9
ℹ suites 0
ℹ pass 9
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 70.188818
@@ -1,17 +0,0 @@
✔ free text renders as itself with no citations (1.211164ms)
✔ a cited reply renders its text, then each cite with the badge from the check result (0.457937ms)
✔ a parsed body renders as its string does (0.255138ms)
✔ without a check result a path cite has no badge, no label and is never changed (0.189285ms)
✔ the label and the badge come from the check result only, never from a bad entry (0.162195ms)
✔ a web cite is a link only for https (0.231665ms)
✔ a write notice renders its fixed line (0.167596ms)
✔ a body without the reply or notice shape is free text: the whole body, no citations (0.283804ms)
✔ an empty citations list is a reply with no cites (0.145774ms)
ℹ tests 9
ℹ suites 0
ℹ pass 9
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 73.226696
@@ -1,88 +0,0 @@
TAP version 13
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
---
duration_ms: 18.873338
type: 'test'
...
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
---
duration_ms: 7.061714
type: 'test'
...
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
---
duration_ms: 6.45503
type: 'test'
...
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
---
duration_ms: 6.964122
type: 'test'
...
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
---
duration_ms: 5.745801
type: 'test'
...
# Subtest: the status read takes one project query and answers the bus result for that project
ok 6 - the status read takes one project query and answers the bus result for that project
---
duration_ms: 2.815134
type: 'test'
...
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
---
duration_ms: 7.105984
type: 'test'
...
# Subtest: the status read needs no send; a read-only Console still has no revert
ok 8 - the status read needs no send; a read-only Console still has no revert
---
duration_ms: 2.002948
type: 'test'
...
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
---
duration_ms: 0.708324
type: 'test'
...
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
---
duration_ms: 1.553573
type: 'test'
...
# Subtest: a revert receipt for another project is unknown and is never retried
ok 11 - a revert receipt for another project is unknown and is never retried
---
duration_ms: 1.371025
type: 'test'
...
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
---
duration_ms: 1.586864
type: 'test'
...
# Subtest: a configured revert callback remains unreachable when send is off
ok 13 - a configured revert callback remains unreachable when send is off
---
duration_ms: 1.226995
type: 'test'
...
1..13
# tests 13
# suites 0
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 177.384993
@@ -1,20 +0,0 @@
✔ a signed-in same-origin revert reaches exactly one library.revert and returns the new commit (19.266366ms)
✔ a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (7.415028ms)
✔ a revert takes a project and a 40-hex commit, nothing else, before the bus is asked (6.176121ms)
✔ a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal (7.325625ms)
✔ a revert whose answer is lost or unusable is reported unknown and is never retried (5.0635ms)
✔ the status read takes one project query and answers the bus result for that project (3.008847ms)
✔ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (6.314096ms)
✔ the status read needs no send; a read-only Console still has no revert (1.440122ms)
✔ library routes are the owner's: a granted user is refused them, and the tester never reverts (0.563979ms)
✔ the page gets the one library renderer, byte for byte, as a JavaScript module (1.180551ms)
✔ a revert receipt for another project is unknown and is never retried (1.12255ms)
✔ a granted-user HTTP session cannot reach owner library callbacks (1.351581ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 182.202771
@@ -1,76 +0,0 @@
✔ a signed-in same-origin revert reaches exactly one library.revert and returns the new commit (18.853567ms)
✔ a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (6.491576ms)
✔ a revert takes a project and a 40-hex commit, nothing else, before the bus is asked (5.647113ms)
✔ a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal (6.08423ms)
✔ a revert whose answer is lost or unusable is reported unknown and is never retried (5.445934ms)
✖ the status read takes one project query and answers the bus result for that project (3.470803ms)
✖ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (5.42219ms)
✔ the status read needs no send; a read-only Console still has no revert (1.428304ms)
✔ library routes are the owner's: a granted user is refused them, and the tester never reverts (0.509039ms)
✔ the page gets the one library renderer, byte for byte, as a JavaScript module (1.123365ms)
✖ a revert receipt for another project is unknown and is never retried (1.209291ms)
✔ a granted-user HTTP session cannot reach owner library callbacks (1.398731ms)
ℹ tests 12
ℹ suites 0
ℹ pass 9
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 198.061254
✖ failing tests:
test at packages/webui/tests/library-routes.test.mjs:100:1
✖ the status read takes one project query and answers the bus result for that project (3.470803ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
403 !== 405
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:107:10)
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) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 403,
expected: 405,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/webui/tests/library-routes.test.mjs:111:1
✖ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (5.42219ms)
AssertionError [ERR_ASSERTION]: project-not-declared
400 !== 502
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:119:12)
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) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 400,
expected: 502,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/webui/tests/library-routes.test.mjs:145:1
✖ a revert receipt for another project is unknown and is never retried (1.209291ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
200 !== 503
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:148:10)
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) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 200,
expected: 503,
operator: 'strictEqual',
diff: 'simple'
}
@@ -1,170 +0,0 @@
diff --git a/packages/bus/tests/channel-proof.test.mjs b/packages/bus/tests/channel-proof.test.mjs
index ba0179832..435d8fa2d 100644
--- a/packages/bus/tests/channel-proof.test.mjs
+++ b/packages/bus/tests/channel-proof.test.mjs
@@ -11,0 +12,2 @@
+// Row 225 (C2-10b) re-pins webui bus.mjs for one STATUS entry, library-revert-not-agent: 409; no proof
+// or transport change.
@@ -32 +34 @@ const baselines = {
- "packages/webui/src/bus.mjs": "604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c",
+ "packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
diff --git a/packages/library/tests/render.test.mjs b/packages/library/tests/render.test.mjs
index b07e26d25..9c861bfe9 100644
--- a/packages/library/tests/render.test.mjs
+++ b/packages/library/tests/render.test.mjs
@@ -3 +3 @@ import assert from 'node:assert/strict';
-import { renderItems, UNREADABLE } from '../src/index.mjs';
+import { renderReply } from '../src/index.mjs';
@@ -4,0 +5 @@ import { renderItems, UNREADABLE } from '../src/index.mjs';
+// Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2).
@@ -6,6 +7,12 @@ const A = 'a'.repeat(40), B = 'b'.repeat(40);
-const cite = (path, lines, quote, commit = A) => ({ path, commit, lines, quote });
-const options = {
- scopeOf: path => path.includes('/Playground/') ? 'playground' : path.endsWith('.md') ? 'canon' : undefined,
- changed: ({ path, commit }) => path === 'Book One/Characters/Mara Vell.md' && commit === A,
-};
-const render = items => renderItems(JSON.stringify({ items }), options);
+const cite = (path, lines, quote, commit = A, extra = {}) => ({ path, commit, lines, quote, ...extra });
+const web = (url, quote = 'spring tides') => ({ url, quote });
+const reply = (text, citations) => JSON.stringify({ text, citations });
+const CITED = reply("Mara's eyes are grey in chapter 3.", [
+ cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', A, { as: 'canon' }),
+ cite('Book One/Playground/What If.md', [5, 6], 'a storm', B),
+ cite('Notes/Eyes.md', [9, 10], 'green eyes'),
+ web('https://example.invalid/tides'),
+]);
+const CHECK = { citations: [{ scope: 'canon', agent: null }, { scope: 'playground', agent: null },
+ { scope: 'supporting', agent: 'acme/scribe' }, { scope: 'web', agent: null }] };
+const changed = ({ path, commit }) => path === 'Notes/Eyes.md' && commit === A;
@@ -13,21 +20,4 @@ const render = items => renderItems(JSON.stringify({ items }), options);
-// One snapshot per item kind: the plain text both channels show.
-const SNAPSHOTS = {
- fact: [{ kind: 'fact', text: 'Tomas is the ferryman.', cites: [cite('Book One/Characters/Tomas.md', [3, 3], 'the ferryman', B)] }],
- finding: [{ kind: 'finding', text: 'Eye colour differs.', cites: [cite('Book One/Chapters/Chapter 01.md', [41, 41], 'her grey eyes'),
- cite('Book One/Characters/Mara Vell.md', [9, 10], 'green eyes')] }],
- correction: [{ kind: 'correction', text: 'Chapter 1 says grey.', from: 'green eyes', to: 'grey eyes',
- cites: [cite('Book One/Characters/Mara Vell.md', [9, 9], 'green eyes', B)] }],
- question: [{ kind: 'question', text: 'Is the harbor north of the town?', cites: [cite('Book One/Places/Harbor Town.md', [2, 4], 'the harbor', B)] }],
- idea: [{ kind: 'idea', text: 'A storm could close the harbor.', cites: [cite('Book One/Playground/What If.md', [5, 5], 'a storm', B),
- { url: 'https://example.invalid/tides', quote: 'spring tides' }] }],
-};
-const EXPECTED = {
- fact: '1. Fact: Tomas is the ferryman.\n [canon] Book One/Characters/Tomas.md:3 @bbbbbbb "the ferryman"',
- finding: '1. Finding: Eye colour differs.\n [canon] Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
- + ' [canon] Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes" (changed since)',
- correction: '1. Correction: Chapter 1 says grey.\n From: "green eyes"\n To: "grey eyes"\n'
- + ' [canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes"',
- question: '1. Question: Is the harbor north of the town?\n [canon] Book One/Places/Harbor Town.md:2-4 @bbbbbbb "the harbor"',
- idea: '1. Idea: A storm could close the harbor.\n [playground] Book One/Playground/What If.md:5 @bbbbbbb "a storm"\n'
- + ' [web] https://example.invalid/tides "spring tides"',
-};
+test('free text renders as itself with no citations', () => {
+ for (const body of ['Grey, in chapter 3.', '', '"a JSON string"', '[1, 2]', '42', 'null'])
+ assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body });
+});
@@ -35,8 +25,15 @@ const EXPECTED = {
-for (const kind of Object.keys(SNAPSHOTS)) {
- test(`renderItems snapshot: ${kind}`, () => {
- const out = render(SNAPSHOTS[kind]);
- assert.equal(out.kind, 'items');
- assert.equal(out.text, EXPECTED[kind]);
- assert.equal(out.items[0].kind, kind);
- });
-}
+test('a cited reply renders its text, then each cite with the badge from the check result', () => {
+ const out = renderReply(CITED, { check: CHECK, changed });
+ assert.equal(out.kind, 'reply');
+ assert.equal(out.text, "Mara's eyes are grey in chapter 3.");
+ assert.deepEqual(out.citations.map(c => c.badge), ['canon', 'playground', 'supporting', 'web']);
+ assert.equal(out.plain, "Mara's eyes are grey in chapter 3.\n"
+ + ' [canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"\n'
+ + ' [playground] Book One/Playground/What If.md:5-6 @bbbbbbb "a storm"\n'
+ + ' [supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)\n'
+ + ' [web] https://example.invalid/tides "spring tides"');
+ assert.deepEqual(out.citations[0], { badge: 'canon', path: 'Book One/Chapters/Chapter 03.md', commit: A, lines: [41, 41],
+ ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', quote: 'her grey eyes', canon: true, agent: null, changed: false });
+ assert.deepEqual(out.citations[3], { badge: 'web', url: 'https://example.invalid/tides', link: 'https://example.invalid/tides',
+ ref: 'https://example.invalid/tides', quote: 'spring tides', agent: null, changed: false });
+});
@@ -44,6 +41,2 @@ for (const kind of Object.keys(SNAPSHOTS)) {
-test('renderItems numbers several items and takes a parsed body as well as a string', () => {
- const items = [...SNAPSHOTS.fact, ...SNAPSHOTS.question];
- const out = renderItems({ items }, options);
- assert.equal(out.text, `${EXPECTED.fact}\n${EXPECTED.question.replace(/^1\./, '2.')}`);
- assert.deepEqual(out.items[1].cites[0], { badge: 'canon', path: 'Book One/Places/Harbor Town.md', commit: B, lines: [2, 4],
- ref: 'Book One/Places/Harbor Town.md:2-4 @bbbbbbb', quote: 'the harbor', changed: false });
+test('a parsed body renders as its string does', () => {
+ assert.deepEqual(renderReply(JSON.parse(CITED), { check: CHECK, changed }), renderReply(CITED, { check: CHECK, changed }));
@@ -52,4 +45,4 @@ test('renderItems numbers several items and takes a parsed body as well as a str
-test('without options a path cite has no badge and is never marked changed', () => {
- assert.equal(renderItems({ items: SNAPSHOTS.finding }).text,
- '1. Finding: Eye colour differs.\n Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
- + ' Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes"');
+test('without a check result a path cite has no badge, no label and is never changed', () => {
+ const out = renderReply(CITED);
+ assert.deepEqual(out.citations.map(c => [c.badge, c.agent, c.changed]),
+ [[null, null, false], [null, null, false], [null, null, false], ['web', null, false]]);
@@ -58,6 +51,6 @@ test('without options a path cite has no badge and is never marked changed', ()
-const NOTICE_SNAPSHOTS = { 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' };
-for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
- test(`renderItems snapshot: notice ${notice}`, () => {
- assert.deepEqual(renderItems(JSON.stringify({ items: [], notice }), options), { kind: 'notice', notice, text });
- });
-}
+test('the label and the badge come from the check result only, never from a bad entry', () => {
+ const bad = { citations: [{ scope: 'web', agent: '' }, { scope: 'story', agent: 7 }, null, { scope: 'canon', agent: 'x/y' }] };
+ const out = renderReply(CITED, { check: bad });
+ // A path cite can't be web; a url cite is web by its shape whatever the check says, and never agent-written.
+ assert.deepEqual(out.citations.map(c => [c.badge, c.agent]), [[null, null], [null, null], [null, null], ['web', null]]);
+});
@@ -65,14 +58,8 @@ for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
-test('a body without the reply shape shows a fixed line and none of its text', () => {
- const secret = 'free prose the guard would refuse';
- const bodies = [secret, JSON.stringify(secret), '{"items": [], "notice": "other"}', JSON.stringify({ items: [], notice: 'no-answer', extra: secret }),
- JSON.stringify({ items: [] }), JSON.stringify({ items: [{ kind: 'story', text: secret, cites: [] }] }),
- JSON.stringify({ items: [{ kind: 'fact', text: secret }] }), JSON.stringify({ items: SNAPSHOTS.fact, prose: secret }),
- JSON.stringify({ items: [{ kind: 'correction', text: secret, cites: [] }] }),
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [cite('a.md', [2, 1], 'q')] }] }),
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }] }] }),
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }] }] }),
- null, 42, '[]'];
- for (const body of bodies) {
- const out = renderItems(body, options);
- assert.deepEqual(out, { kind: 'unreadable', text: UNREADABLE }, String(body));
- assert.ok(!JSON.stringify(out).includes(secret));
+test('a web cite is a link only for https', () => {
+ const urls = { 'https://example.invalid/a': 'https://example.invalid/a', 'http://example.invalid/a': null,
+ 'javascript:alert(1)': null, 'JAVASCRIPT:alert(1)': null, 'data:text/html,x': null, 'not a url': null, '//example.invalid': null };
+ for (const [url, link] of Object.entries(urls)) {
+ const out = renderReply(reply('t', [web(url)]));
+ assert.equal(out.kind, 'reply', url);
+ assert.equal(out.citations[0].link, link, url);
+ assert.equal(out.citations[0].ref, url);
@@ -80,0 +68,27 @@ test('a body without the reply shape shows a fixed line and none of its text', (
+
+test('a write notice renders its fixed line', () => {
+ const body = op => JSON.stringify({ notice: 'library-write', op, path: 'Notes/Eyes.md', commit: B });
+ assert.deepEqual(renderReply(body('write'), { name: 'Scribe' }),
+ { kind: 'notice', op: 'write', path: 'Notes/Eyes.md', commit: B, text: 'Scribe wrote Notes/Eyes.md @bbbbbbb', plain: 'Scribe wrote Notes/Eyes.md @bbbbbbb' });
+ assert.equal(renderReply(body('delete'), { name: 'Scribe' }).text, 'Scribe deleted Notes/Eyes.md @bbbbbbb');
+ assert.equal(renderReply(body('write')).text, 'An agent wrote Notes/Eyes.md @bbbbbbb');
+});
+
+test('a body without the reply or notice shape is free text: the whole body, no citations', () => {
+ const bodies = [
+ JSON.stringify({ text: 't' }), JSON.stringify({ citations: [] }), JSON.stringify({ text: 1, citations: [] }),
+ JSON.stringify({ text: 't', citations: [], extra: 1 }), JSON.stringify({ items: [] }),
+ reply('t', [cite('a.md', [2, 1], 'q')]), reply('t', [cite('a.md', [0, 1], 'q')]), reply('t', [cite('a.md', [1], 'q')]),
+ reply('t', [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }]), reply('t', [cite('a.md', [1, 1], 'q', A, { as: 'playground' })]),
+ reply('t', [cite('a.md', [1, 1], 'q', A, { scope: 'canon' })]), reply('t', [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }]),
+ reply('t', [{ url: 'https://x.invalid' }]), reply('t', [{ url: 'https://x.invalid', quote: 'q', agent: 'x/y' }]),
+ JSON.stringify({ notice: 'library-write', op: 'move', path: 'a.md', commit: A }),
+ JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: 'abc' }),
+ JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: A, by: 'x' }),
+ ];
+ for (const body of bodies) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }, body);
+});
+
+test('an empty citations list is a reply with no cites', () => {
+ assert.deepEqual(renderReply(reply('Grey.', [])), { kind: 'reply', text: 'Grey.', citations: [], plain: 'Grey.' });
+});
@@ -1,13 +0,0 @@
6bdaf870be3d8d03a68eb03f5f6916150bb1b3db134ffbe84018e91d47e6434d packages/bus/tests/channel-proof.test.mjs
0385fc49b445ff730021eb11c20b29027631e0d27350e4591d5a0936ffbdccf6 packages/library/README.md
653ff444ba66cb73e1ca4df1f6037f15a2ec7d3d009cf614a0dc51deabda4b50 packages/library/src/index.mjs
dfcc9f271afc7afdab908395a7f3c402100fa89b4994b20d5bcae3140d5c335e packages/library/src/render.mjs
f3c213f3b0bd080d0bc5837f741a29e324acb91cf0c27144dee08a4113262210 packages/library/tests/render.test.mjs
9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658 packages/webui/src/bus.mjs
f1a0c9a55b1ae933e205c7e2b9753afe0c2f1fc9b0faa2cf18765396f99ac8dd packages/webui/src/cli.mjs
bf49a22147be0535076640d20149116a744dad9b0b32dd667ee297a061d56134 packages/webui/src/console-bus.mjs
793e7dd9edbe5a55c2deab3c8aef4fbd5b4c838e132e2ac9294043c7176ec6f7 packages/webui/src/public/chat.css
af3a6f382e0d40cf2b28c31db7cb58ba775f1e242651ba42c7426bda99dc0d89 packages/webui/src/public/chat.js
6412fe34bc640433e9cf7610f65d3574a3415c934354d8cb52e056793a81948f packages/webui/src/serve.mjs
f191752f867a112c867ca24268f70aa99ca0903c9208f76bc4bb4c1587cb55df packages/webui/tests/chat-replies-browser.test.mjs
df4ff385ac4ca8244ffa00faf82fcca1f4fcdac8da02a4f4d71233d4df8e1157 packages/webui/tests/library-routes.test.mjs
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""Run row225 mutants in disposable snapshots, only on Sage CLEAR.
Commit the tested candidate first. Never change the worktree or shared dependencies.
A timeout, import failure or process failure is not a mutation kill.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import tempfile
BROWSER = 'packages/webui/tests/chat-replies-browser.test.mjs'
ROUTES = 'packages/webui/tests/library-routes.test.mjs'
CHAT = 'packages/webui/src/public/chat.js'
SERVE = 'packages/webui/src/serve.mjs'
RENDER = 'packages/library/src/render.mjs'
MUTANTS = [
('M1-badges', RENDER, 'badge: scope, path:', 'badge: null, path:', BROWSER),
('M1-agent-label', RENDER, 'canon: c.canon, agent,', 'canon: c.canon, agent: null,', BROWSER),
('M2-http-links', RENDER, "new URL(url).protocol === 'https:' ? url : null", 'url', BROWSER),
('M3-wrong-commit', CHAT, 'e.file !== e.last', 'cite.commit !== e.last', BROWSER),
('M3-missing-file', CHAT, "SHA.test(e?.file ?? '') && SHA.test(e?.last ?? '')", "SHA.test(e?.last ?? '')", BROWSER),
('M4-fallback', CHAT, 'Array.isArray(limits.fallback) ? limits.fallback.filter', 'Array.isArray(v.replyLimitsFallback) ? v.replyLimitsFallback.filter', BROWSER),
('M4-problem', CHAT, "if (CODE.test(limits.problem ?? '')) parts.push(`<code>${txt(limits.problem)}</code>`);", '', BROWSER),
('M5-send', SERVE, 'if (!send || !libraryRevert || !named)', 'if (!libraryRevert || !named)', ROUTES),
('M5-project-receipt', SERVE, 'result.project !== value.project || ', '', ROUTES),
('M5-commit-input', SERVE, '!COMMIT.test(value.commit)', 'false', ROUTES),
('M5-origin', SERVE, "if (one(req, 'origin') !== ownOrigin(preview)) return refuse(403, 'cross-origin request refused', 'a revert needs the Console\\'s own Origin');", '', ROUTES),
('M5-fetch-metadata', SERVE, "if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site request refused', 'a revert needs same-origin Fetch Metadata');", '', ROUTES),
('M6-duplicate-call', SERVE, 'result = await libraryRevert({ project: value.project, commit: value.commit });', 'result = await libraryRevert({ project: value.project, commit: value.commit }); await libraryRevert({ project: value.project, commit: value.commit });', ROUTES),
('M7-user-fence', SERVE, "if (path === '/api/chat' || path.startsWith('/api/chat/')) return true;", "if (path.startsWith('/api/library/')) return true; if (path === '/api/chat' || path.startsWith('/api/chat/')) return true;", ROUTES),
]
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--clear', required=True, help='Sage CLEAR authorizing this run')
parser.add_argument('--output', required=True, type=Path)
args = parser.parse_args()
repo = Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip())
if subprocess.check_output(['git', 'status', '--porcelain', '--', 'packages'], text=True).strip():
raise SystemExit('Commit tested packages first: snapshot must match candidate.')
head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
before = {p: digest(repo / p) for p in [CHAT, SERVE, RENDER]}
args.output.mkdir(parents=True, exist_ok=True)
results = []
with tempfile.TemporaryDirectory(prefix='mosaic-row225-mutants-') as directory:
scratch = Path(directory)
archive = scratch / 'candidate.tar'
with archive.open('wb') as stream:
subprocess.run(['git', 'archive', 'HEAD', 'packages', 'roles', 'contracts', 'tasks', 'missions', 'package.json', 'RELEASE'], stdout=stream, check=True)
for name, target, old, new, test in MUTANTS:
tree = scratch / name
tree.mkdir()
with tarfile.open(archive) as bundle:
bundle.extractall(tree, filter='data')
(tree / 'node_modules').symlink_to((repo / 'node_modules').resolve(), target_is_directory=True)
path = tree / target
source = path.read_text()
hits = source.count(old)
if hits != 1:
raise SystemExit(f'{name}: expected one anchor, found {hits}')
path.write_text(source.replace(old, new, 1))
command = ['timeout', '-k', '10', '240' if test == BROWSER else '120', 'node', '--test', '--test-reporter=tap', '--test-concurrency=1', '--test-timeout=30000', test]
run = subprocess.run(command, cwd=tree, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ)
log = args.output / f'{name}.tap'
log.write_text(run.stdout)
path.write_text(source)
restored = digest(path) == hashlib.sha256(source.encode()).hexdigest()
worktree_clean = before == {p: digest(repo / p) for p in before}
if not restored or not worktree_clean:
raise SystemExit(f'{name}: restore/hash check failed')
killed = run.returncode == 1 and 'ERR_ASSERTION' in run.stdout and 'not ok ' in run.stdout
results.append({'mutant': name, 'anchorHits': hits, 'restored': restored, 'worktreeUnchanged': worktree_clean, 'exit': run.returncode, 'killed': killed, 'test': test, 'log': str(log)})
print(json.dumps(results[-1]), flush=True)
shutil.rmtree(tree)
if not killed:
break
after = {p: digest(repo / p) for p in before}
report = {'head': head, 'clear': args.clear, 'worktreeUnchanged': before == after, 'results': results}
(args.output / 'summary.json').write_text(json.dumps(report, indent=2) + '\n')
if before != after or len(results) != len(MUTANTS) or not all(r['killed'] for r in results):
raise SystemExit(1)
if __name__ == '__main__':
main()
+266
View File
@@ -0,0 +1,266 @@
# Row 296: FED R7, scratch two-instance acceptance (#1802)
## Start
- Started 2026-10-23 18:53:27Z, on Sage's sage-3583 (mos-1489).
- Base: origin/refactor 45dca69f6 (pinned; local copy used, no fetch: fetch runs the credential node script, which
is held during Darkwing's re-pin window).
- Branch: quackerjack/row296 in worktree quackerjack-fed-r5-row296.
- Brief: docs/plans/2026-10-23_mvp-readiness-rows.md at 45dca69f6, section "Brief: FED R7, scratch two-instance
acceptance" (line 77).
- Constraints from Sage: scratch roots only (mktemp -d under /mnt/storage/scratch/tmp), never ~/.mosaic-dev or
the live bus. No push, claim, queue write, node or test run until CLEAR.
## What the brief asks for (summary)
- One script test under scripts/ with its harness in packages/*/tests/, as row 190's tests are.
- Two configs, two data roots, two Consoles, one fake OP. Consoles start outside agent ancestry in a transient
user unit. They never use the live stack's units, ports or sockets. The script removes its units on exit (trap).
- Test (design section 7 R7 gate): Jason's test identity, granted collaborator on one hub project, signs in,
makes a thread, sends to the granted instance, gets the reply in the thread, is refused on another project and
on board reads, is revoked, is refused again, and the run ends. The factory shows nothing of the hub.
- Mutants: grant project list widened to `*`; revocation claim end skipped.
- Gate: Filbert approves the manifest; the script test and every scripts/test-*.sh green on Sage's gate; no unit,
port or socket of the script outlives the run. R8 (live window) stays row 209.
## Open reads before design
- R4 and R6 candidates (the brief says it builds on their combined candidate). Confirm what they expose: the
hub sign-in, grant and revocation paths, and the Console's board-read refusal.
- Row 190's test harness (several instances on one uid): how it starts units and cleans up.
- Transient user unit support on this host (systemd-run --user), and how the existing tests detect it.
## Status
- 18:53Z: worktree created, brief read. No code yet.
## Handoff 2026-10-23 19:10:09Z (Mos ruling Q245, mos-1503)
Row 296 moves to Lena (Codex seat, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a, branch lena/row296). Quackerjack
stops here: no builds, no claim, no queue writes. This design is the handoff; Lena continues it.
Base: 45dca69f6. R4 landed as 2efd127ce (ancestor of the base, checked with merge-base --is-ancestor). R6 landed as
d3059c2c3 (row 294). R7 is claimable once Darkwing's window ends (Sage, sage-3592).
What I verified by reading (not by running):
- packages/cli/src/access.mjs:65 reads `system.federation?.peers ?? []` (the R6 seam).
- packages/bus/src/user-cli.mjs exists at base (the user-cli transport).
- Row 190's tests (packages/cli/tests/stack-instances.test.mjs, stack-release.test.mjs T56, stack-release-fixture.mjs)
drive stand-in units and fixtures. They do not start real transient user units. Not a model for R7's Consoles.
- Real transient-unit launch pattern: packages/conversation/src/cohort.mjs (`scopeAvailable()` and `ScopeLauncher`,
systemd-run --user --scope -p Delegate=yes --unit=...; MANAGER_ENV = XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS).
- Test-only spawn of systemd-run in packages/conversation/tests is the nearest precedent for start/stop cleanup.
What I did NOT verify (so treat as open): the hub sign-in, grant, revocation and board-read paths in R4/R6 code; the
Console startup flags; whether the fake OP can be a local stand-in; the trap-based unit cleanup.
Design (proposal, not yet checked against code):
1. scripts/test-federation-acceptance.sh: the entry point. Creates a mktemp -d scratch root under
/mnt/storage/scratch/tmp (never ~/.mosaic-dev, never the live bus). trap removes every unit it started and the
scratch root. Refuses to run if a transient unit name collides.
2. Two configs (instance A = hub-side granted seat, instance B = collaborator's home), each with its own
MOSAIC_CONFIG, data root and federation.self; federation.peers names the other.
3. One fake OP: a local stand-in that issues Jason's test identity. Its port and socket are per-run, not the live ones.
4. Two Consoles, each in its own transient user unit (systemd-run --user --unit=mosaic-r7-<run>-<A|B>); outside agent
ancestry; ports picked per run.
5. Test flow (the R7 gate): identity -> grant on one hub project -> sign in -> thread -> send to granted instance ->
reply in thread -> refused on another project -> refused on board reads -> revoke -> refused after revoke -> run
ends. Factory output must show nothing of the hub.
6. Mutants: (a) grant project list widened to `*`, must fail the other-project refusal; (b) revocation claim end
skipped, must fail the refusal-after-revoke check.
Unresolved decisions for Lena (I did not decide these):
- Whether the harness lives in packages/cli/tests (matching row 190) or packages/conversation/tests (matching
cohort.mjs). Brief says packages/*/tests/; pick the one that owns the Console start path.
- Fake OP: in-process HTTP stand-in versus file-backed identity. No preference recorded.
- Port selection: fixed range per run vs OS-assigned. Brief forbids the live ports; it does not pick a range.
R2 and R5 learning:
- R2 (row 203, #1701) landed as a4da4e6df. I built it. Relevant to R7: validateFederation in
scripts/mosaic-config.mjs rejects unknown keys and bad ids, and test-config.sh has an FED block
(expect_fed_refusal). Use federation.self/peers exactly as row 203 validates them.
Mutant runner pattern: one textual edit that must occur exactly once, then a named test must fail; restore from git.
Name filter gotcha: `node --test --test-name-pattern` skips subtests whose parent does not match; include the
parent name (T57 for row 203).
- R5: I did not build R5 in this session and have no first-hand notes on it. Ask Sage for R5's landing sha and notes.
Status: design written; no code, no test runs, no claim, no push of the test files. Next owner: Lena.
## Lena continuation, 2026-10-23
Owner: Lena, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a. Branch lena/row296.
Base remains 45dca69f6. Quackerjack's handoff c6ab9bdc9 preserved by cherry-pick ad84c1e45.
Issue #1802 exists. Reviewer Filbert. Sage owns queue state, shared test slot and landing.
Status: Untested. All Node, tests and credential-helper fetch/push held for Sage CLEAR (sage-3615).
The earlier canonical `queue next lena` read finished with `nothing` and a rendered-view warning;
no queue state was changed. Lena's owner registration is Sage's action.
### Design decisions sent to Sage (lena-0003)
- Harness under packages/cli/tests, beside row 190's instance proof; shell entry under scripts/.
- Existing HTTPS fakeOidc helper, its generated scratch CA trusted through NODE_EXTRA_CA_CERTS
by each real Console CLI. One fake OP, two client ids; no TLS verification bypass.
- Kernel-assigned loopback ports (0), discovered from each Console's startup log. Public origins
are distinct *.example.test HTTPS names; HTTP requests model the loopback reverse proxy with
exact Host and Origin. This proves the Console boundary, not a deployed HTTPS front end (R8).
- Transient user service units, not scopes: a scope leaves ancestry beneath the caller; the real
user proof walks ancestry. Each Console runs the existing cli.mjs serve --sso --send verb.
- Real startHost broker process and createLauncher. Only the harness adapter is a test stand-in,
as in thread-revoke.test.mjs. A real sandboxed run replies through the broker.
- Scratch-only setup and access grant/revoke use real human CLI transport from a transient service.
No injected principal verifier, direct bindHuman/bindUser, or simulated user transport.
- Every service has a unique per-run prefix and is recorded before launch. Shell EXIT/INT/TERM
traps stop only these services, wait for collection and verify socket and port closure.
- Claim-end assertion reads append-only claim history independently of the launcher's final
shutdown so the claim-end mutant cannot be hidden by endLaunch's cleanup.
### Implementation plan
Goal: an executable scratch proof of federation section 7's R7 gate, without live state.
Architecture: shell lifecycle owner, Node test driver, test-only host fixture, existing real Console CLI.
Tech: Bash, Node ESM/node:test, systemd user transient services, existing fake OP and bwrap adapter.
Spec: docs/plans/2026-10-23_mvp-readiness-rows.md, FED R7, plus preserved handoff above.
Constraints: scratch only under /mnt/storage/scratch/tmp; no live units/ports/sockets/configs or
credential reads; no production behavior/policy changes; no old test edits without Mos via Sage.
Review focus: partial startup cleanup; signals; unit name collision; mismatched hub/factory sessions;
claim end versus process exit. Each must have explicit assertions or a negative cleanup exercise.
- [ ] Write acceptance driver assertions: Jason test sub binds as collaborator (owner is another
fixture identity), one allowed project/instance, thread send and correlated RESULT, private
project creation/read refused, board reads refused, factory has no hub objects, revoke denies
session and re-login, claim history records access-revoked and runner ends.
- [ ] Write shell lifecycle: scratch HOME/TMPDIR, unique unit ledger, bounded command timeout,
traps stop/collect only this run's units, verify recorded sockets and ports no longer listen.
- [ ] Write host fixture using startHost(bootConfig), createLauncher with fakeHarnesses and the
existing thread adapter. Prepare two named configs through bootstrap before fixture edits;
supply only generated test client slots. Setup/grant/revoke through real human transport.
- [ ] On Sage CLEAR run only the new script, fix failures in new files and record exact results.
- [ ] Prepare explicit once-only mutant patches: R5 projects widened to ['*']; R4 #claimEnd call
skipped. On Sage CLEAR prove each gives an assertion failure, restore bytes in finally.
- [ ] Publish manifest (paths, hashes, assertion mapping, old-test numstat), request Filbert review,
push additive commits to lena/row296 only. Sage runs shared suites and gate and lands the row.
Pre-existing test changes: none (numstat: empty).
### Source inspection corrections before first execution
- The real webui chat is channel-gated. Scratch businesses enable only harness pi and channel webui;
the loader fixture re-exports the unchanged webui channel. The adapter replacement is exactly
fakeHarnesses' pi.adapter export; no product file gains a fixture branch.
- startHost creates a real broker child and a real Unix proof socket; it is hosted in the test
driver. The two Console services run the exact same-tree webui CLI, which is required by
verifyPrincipal. Human setup runs the exact same-tree human-cli through humanTransport.
- The earlier in-process host proposal did not need a separate host fixture: startHost exposes
the production launcher API directly, so the only helper file is federation-human.mjs.
- Read-only SQLite inspects claim evidence while the broker remains the sole writer. The test
also checks the registered runner and engine pids/start times are gone after revocation.
- `--kill-after-start` kills the test process after both services have listened; Node teardown
hooks cannot run. The shell trap owns service cleanup independently. This is an expected-red
exercise; acceptance requires its kill marker, nonzero exit and successful cleanup line.
- Main driver environment is a whitelist (PATH, scratch HOME/TMPDIR, systemd manager transport,
R7 run ids). Console/human services use env -i. No host preload or credential env is carried.
- No Node or test has run. Bash syntax and git diff --check passed during preparation.
### Binding design correction: sage-3625, 2026-10-23 19:17:35Z
Sage rejected the transient-unit approach: "no transient systemd user units used to satisfy the
human proof"; such a unit makes the ancestry walk miss the development agent, and creates host
state under mos-1438. This direction replaces the brief/handoff's unit requirement for this build.
No unit was created by the superseded draft, and no Node/test execution occurred.
Current design, replacing the earlier proposals above:
- Consoles are ordinary spawned child processes in their own process groups; no systemd commands,
units, user manager or manager environment are involved.
- Test-only federation-host.mjs follows channel-fixture.mjs's mock.module pattern. It re-exports
real readProcess and replaces verifyHuman's readProcess argument with real process fields plus
ppid=1. This is explicitly a stand-in for Jason's shell, NOT a real ancestry acceptance proof.
Real pid/uid/startTime/nonce/path checks still run; verifyPrincipal still checks the Console's
actual direct parent path for user calls. No product source receives this override.
- Each fixture host runs real startBroker and createLauncher. Its local host API is the existing
thread-revoke.test.mjs pattern, with only the pi adapter replaced via fakeHarnesses.
- Setup/grant/revoke are plain children using real CLI wrappers against that fixture proof seam.
- Shell records each detached group's leader pid and start time, checks both before signalling,
and verifies no live group member, TCP listener or Unix listener remains. Killed-driver mode
bypasses every Node teardown hook and still leaves cleanup to the shell trap.
- Read-only claim evidence and actual runner/engine exit remain independent assertions.
- Gate limitation: this proves the revised scratch integration, not real outside-agent ancestry,
transient unit cleanup, TLS proxy deployment or live federation. R8 remains row 209.
- R5 landing found in git log: ddfb29af7 (#1700).
- Targeted Node/tests remain held. Sage permits the first lena/row296 push in the 19:41Z gap,
with MOSAIC_GITEA_CREDENTIAL_FILE unset. 226 and 283 have earlier slots.
Current files: shell entry; federation-acceptance.test.mjs; fixtures/federation-host.mjs and
fixtures/federation-human.mjs; this BUILD; mutant runner; manifest; append-only logs; preserved
Quackerjack BUILD at ad84c1e45. No pre-existing test modification (numstat empty).
### First candidate, 2026-10-23
Implementation is prepared under sage-3625, not yet executed. Static checks: Bash parser accepts
scripts/test-federation-acceptance.sh; Python ast.parse accepts mutants.py; git diff --check clean.
No test outcome is claimed. The root node_modules links the existing canonical install so the
future targeted run needs no install or network. No webui editor build is involved.
M1 now requires the user project-list leak assertion (`R7 ungranted project must not appear`),
so it proves exposure through the actual Console transport rather than only inspecting grant
configuration. The private-project thread-create refusal and exact grant-list check follow it.
M2 requires `R7 revoke must end the claim as access-revoked` before any host teardown.
Sage opened the push gap early in sage-3644 (19:23:04Z). The first candidate is committed as
Untested, for push only to lena/row296 with MOSAIC_GITEA_CREDENTIAL_FILE unset. Node, acceptance,
negative cleanup exercise and mutants still wait for the test slot. Full suites/gate are Sage's.
### Filbert static review, filbert-296-first-push-static, 19:28:51Z
Review pinned f06a1f78c against 45dca69f6. Filbert verified all nine manifest hashes from Git
objects, no product/pre-existing test changes, no systemd calls, fixture-only ancestry override,
real path/uid/startTime/nonce checks, group ledger identity checks and child-only CA trust.
He ran nothing. The verdict remains pending execution and gate evidence.
- C1 confirmed by inspection: subprocess.run(timeout=280) kills the shell on timeout and can
bypass its trap, while the shell's own deadline plus cleanup can exceed 280 seconds. Removed
the outer Python timeout (the simpler alternative Filbert explicitly allowed). Python now
waits for the shell's bounded run and EXIT/TERM cleanup. Source restoration remains in finally.
- N1 retained limitation: a SIGKILL between spawn and the pid/startTime ledger append can leave
an unrecorded group. The killed-driver exercise begins after both Consoles are recorded and
does not cover that gap. Cleanup claims apply to recorded groups; no universal crash-proof
process creation claim is made.
- N2 addressed before the mutant run: the named readonly claim-history assertion now runs after
revoke and before waiting for the run's end. A skipped #claimEnd therefore cannot first fail
the run-end wait. The final latest-op check still runs after engine/runner exit. If M2 instead
fails an earlier condition, its runner must say NOT KILLED; diagnose the first failure on the
same pinned candidate, retain the intended assertion, and rerun on Sage CLEAR. Never accept a
timeout or change the mutant's expected assertion merely to mark it killed.
- N3 deferred cosmetic fixture detail: wrap() returns fn(value) without await, so async rejection
skips its code mapping. This is not an authorization or cleanup change; the rejection still
propagates, and no verdict relies on that fixture mapping.
- N4: corrected the exact-grant assertion's indentation in the new test.
- N5: prepare() first calls bootstrap --instance, then overwrites that bootstrap-generated
config inside the unique disposable scratch HOME with fixture Console/provider settings.
This is fixture-only setup, not a product/bootstrap overwrite path or a live-config operation.
There remains one selected config per scratch instance, with the same marked data root.
Still Untested. No Node, acceptance, mutant, negative cleanup or CLI suite run. Static validation
only; execution waits for Sage's test window. Follow-up push needs a currently authorized gap.
### Push receipt and N2 ordering proof, 2026-10-23 19:31Z
Sage sage-3659 authorized push before 19:38Z. e855f82e1846f094927c3de69aea1a801253736a
was pushed to origin/lena/row296 with MOSAIC_GITEA_CREDENTIAL_FILE unset. No Node/tests ran.
N2, filbert-296-c1-ack: this path is synchronous after a successful reload. accessCommand awaits
reload at access.mjs:289; reload awaits the access.reload transport at :297. broker.mjs:4645
calls #revokeLost before constructing its reply. #revokeLost (:1025–1034) calls #claimEnd inside
Store.transaction, which executes COMMIT before returning (store.mjs:282–302). server.mjs:160–161
calls broker.request and then writes the response. Therefore the claim record is committed before
the successful CLI call returns; no asynchronous sweep is required for this assertion. A refused
reload can be reported by the CLI with exit 0, but then immediate revocation evidence is absent
and this test must fail; that is not a race the test should hide. No polling change is needed.
T3 package comparison (sage-3659): the four planned runs execute only the new dedicated
federation-acceptance.test.mjs and its fixture host/human children. They do not execute stack-copy,
stack-release or the whole CLI suite. Existing helpers are imported; no old test module is run.
Asked Researcher to confirm against row216. Full CLI suite remains a later Sage gate requirement,
outside the targeted T3 command list. All test resources are unique scratch state.
+67
View File
@@ -0,0 +1,67 @@
# Row 296 / #1802 — C1 follow-up candidate (Untested)
Base: 45dca69f6. Branch: lena/row296. Reviewer: Filbert. Gate owner: Sage.
Handoff: c6ab9bdc9 preserved as ad84c1e45, including
agents/quackerjack/work/queue-296/BUILD.md. Every changed path is in MANIFEST.sha256,
except this manifest and the checksum file itself. No product file or pre-existing test changed.
Pre-existing test numstat: empty. BUILD-LOG.md and docs/SESSIONS.md are append-only additions.
Binding ruling: sage-3625 replaces the brief's transient-unit start with plain child Consoles and
explicit test stand-ins. **This candidate does not prove genuine outside-agent ancestry.**
It creates no transient systemd unit and makes no systemd CLI call. R8 remains row 209.
## What is real, and what is a stand-in
- Two configs created by bootstrap --instance hub/factory, two marked data roots, real Console
cli.mjs serve --sso --send, real broker runtime and launcher, real path-bound human-cli and
user-cli wrappers, real access loader, CLI writes/reload, grant filters and revocation.
- One existing HTTPS fakeOidc server, two issuer/client identities. Its generated CA is trusted
only through NODE_EXTRA_CA_CERTS in this test's child environment, never any trust store.
- federation-host.mjs replaces verifyHuman's readProcess argument with actual process fields plus
ppid=1, exactly the test pattern Sage named. This stands in for Jason's shell on setup,
grant/revoke and both Consoles' transports. The actual Console parent/path check remains.
There is no product parameter, environment switch or altered verifier default.
- fakeHarnesses replaces only the pi harness adapter() with a scratch shell invoking the existing
thread-adapter.mjs. The existing webui channel is re-exported unchanged. No network model runs.
- Public HTTPS origins are modelled through HTTP loopback requests with matching Host/Origin;
reverse-proxy TLS deployment is outside this scratch proof.
## Acceptance map
All checks live in `FED R7 scratch two-instance acceptance` in
packages/cli/tests/federation-acceptance.test.mjs.
| Requirement | Assertion/evidence |
| --- | --- |
| Isolated instances | different config paths/data roots/ports; scratch HOME/TMPDIR; two named markers from bootstrap; port 0, forbidden live ports 7330–7332 checked |
| Jason is collaborator, not owner | owner melanie; access user jason-test, home factory, fake sub jason-test-sub; hub /api/config user=true |
| One project, one granted instance | actual access grant CLI, projects=[shared], instances=[assistant]; user project list contains shared only |
| Thread and reply | create thread on shared, POST message, correlated RESULT with in_reply_to=sent.id, from_role=assistant, to_role=user, body contains question |
| Other project refused | private project absent from listing; create on private returns 403 access-refused |
| Board reads refused | /api/board, /api/bus/inbox and /api/bus/tasks return 403 owner-only |
| Factory sees no hub objects | Jason sub refused at factory; factory owner sees factory-only project and no threads; hub cookie gets 401 at factory |
| Revoke takes effect | actual access revoke CLI; existing hub session 401 and re-login 403 |
| Claim and process ended | live run before revoke; end log exitCode=0; no registered sessions; engine and runner pid/startTime gone; readonly claim history has revoke/access-revoked before the process-end wait, latest op=revoke afterward |
| Normal cleanup | EXIT trap terminates only recorded pid/startTime-checked groups; checks no live group member, no recorded TCP/Unix listener, no other scratch Unix listener, removes scratch directory/socket files |
| Killed-driver cleanup | --kill-after-start prints both-Consoles-started marker then SIGKILLs test driver; shell performs the same independent cleanup; expected nonzero run with successful cleanup line |
## Verification commands (held for Sage CLEAR)
- scripts/test-federation-acceptance.sh — must exit 0, one acceptance test passes, cleanup line present.
- scripts/test-federation-acceptance.sh --kill-after-start — must exit nonzero, print the explicit
interruption marker and `R7 cleanup: no child groups, ports or sockets remain; no units created`.
- python3 agents/lena/work/queue-296/mutants.py M1 — once-only R5 grant projects -> ['*']; must fail
`R7 ungranted project must not appear` and verify cleanup, then restore original source bytes. Python relies on the shell's own run deadline and cleanup; no outer killing timeout.
- python3 agents/lena/work/queue-296/mutants.py M2 — once-only R4 claim end omitted; must fail
`R7 revoke must end the claim as access-revoked` before shutdown, verify cleanup and restore bytes.
- Sage's gate: the new script plus every scripts/test-*.sh; full applicable suites per Sage's slot.
Status: no Node, acceptance or mutant run. Static Bash syntax, Python parsing and diff checks only.
The script never silently skips a missing tool/sandbox/provider: prerequisites fail the acceptance.
Ordinary CLI suites skip the dedicated test until invoked by its owning script.
Static-review follow-up: filbert-296-first-push-static. C1 removes Python's outer timeout so it
cannot SIGKILL the shell before EXIT cleanup. N2 moves the named claim-history assertion before
the process-end wait. N1's spawn-to-ledger SIGKILL gap is retained and documented in BUILD.md;
--kill-after-start exercises the fully recorded groups. N3 remains a cosmetic fixture limitation.
N4 indentation corrected. N5 scratch-bootstrap config overwrite is explicitly fixture-only.
@@ -0,0 +1,9 @@
6e0193ba91cbd20edd3acfbc57ae9af2f016e6d2c3587b434a9085b3a02c493c BUILD-LOG.md
72029331a22f33433ad14ff08fe8b24d59f79e784009b12fd9c5b74c9e524f13 docs/SESSIONS.md
87239327014c49abbe130c17881bb48e13d96545bf9f3e09b6f4aed46e11453b agents/quackerjack/work/queue-296/BUILD.md
6f32d7e5e0c2c1ee086949a68b176a9d1fefb8ad0df562f5ce72b0c01d21e7a0 agents/lena/work/queue-296/BUILD.md
bd6e9fd9e3f7aa0ee6702401776ef7bd3e39562059a11b87aaaf2a427904381b agents/lena/work/queue-296/mutants.py
89a15e62aa6f951596b764993afeff7e33eeaa3a4f45d31599eba7d27a4000ad packages/cli/tests/federation-acceptance.test.mjs
86b76565f5afe9efdd076ef61cc54e40c447d235f77b95818d0d601ce036effa packages/cli/tests/fixtures/federation-host.mjs
b12a9bad4be083f2439c3363738b9763fa0061228f84d501a385ade48fea90e4 packages/cli/tests/fixtures/federation-human.mjs
4008361f062a2f21b6075c500060196e10a48351fd7d95b25d681f36b4fa801a scripts/test-federation-acceptance.sh
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""R7 mutants. Run only on Sage CLEAR in Lena's isolated candidate.
Every replacement must occur once. Product bytes are restored even on timeout
or failure. A killed mutant must contain the named assertion in the script's
output; startup errors and timeout alone are not evidence. The shell owns its
own timeout and EXIT/TERM cleanup. Do not put a killing Python timeout around it.
"""
from pathlib import Path
import subprocess
import sys
repo = Path(__file__).resolve().parents[4]
mutants = {
'M1': (
'packages/cli/src/access.mjs',
'projects: resolveProjects(list(parsed.values["--projects"]), rows),',
'projects: ["*"],',
'R7 ungranted project must not appear',
),
'M2': (
'packages/bus/src/broker.mjs',
"this.#claimEnd({ business: t.business }, h, 'revoke', null, 'access-revoked', by);",
"/* R7 mutant: skip ending the revoked claim */",
'R7 revoke must end the claim as access-revoked',
),
}
if len(sys.argv) != 2 or sys.argv[1] not in mutants:
raise SystemExit('usage: mutants.py M1|M2')
name = sys.argv[1]
file, before, after, assertion = mutants[name]
path = repo / file
original = path.read_bytes()
text = original.decode()
if text.count(before) != 1:
raise SystemExit(f'{name}: anchor count {text.count(before)}, expected 1')
try:
path.write_text(text.replace(before, after))
result = subprocess.run(['scripts/test-federation-acceptance.sh'], cwd=repo,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True)
print(result.stdout, end='')
if result.returncode == 0 or assertion not in result.stdout:
raise SystemExit(f'{name}: NOT KILLED by expected assertion')
if 'R7 cleanup: no child groups, ports or sockets remain; no units created' not in result.stdout:
raise SystemExit(f'{name}: cleanup not verified')
print(f'{name}: KILLED by {assertion}')
finally:
path.write_bytes(original)
if path.read_bytes() != original:
raise SystemExit(f'{name}: restore failed')
-177
View File
@@ -1,177 +0,0 @@
# Row 225 (C2-10b) Console chat: replies, citations, write notices, status line
Owner: Morgana, from sage-3582 (18:53Z 10-23). Base: origin/refactor 45dca69f6. Branch: morgana/row225.
Brief: docs/plans/2026-10-20_case2-rows.md, C2-10b. Design: agents/gosalyn/work/case2/DESIGN.md 7.4,
5.2 (the reply schema and the check result) and the write notice and `library.revert` (about line 286).
## Ruling on the dependency gap (sage-3591)
225 builds against the design contracts. `renderReply` stays pure, and the caller supplies the answers
it can't know. The revert route proxies to the bus verb and shows the refusal code until the verb
exists. Landing order: 225 lands after 217 (C2-06), not after 288 (C2-02). Sage adds `after 217` to
row 225. Before 288 lands, the Revert button gets the bus's refusal and shows its code, as the brief
allows.
## A1. The check result I assume (to 217, Launchpad)
The broker stores one check result with each agent-to-human message the guard passes. The Console reads
it on the message row that `/api/chat/threads/<id>/messages` returns, under the key `check`:
```json
{
"check": {
"citations": [
{ "scope": "canon", "agent": null },
{ "scope": "supporting", "agent": "acme/scribe" },
{ "scope": "web", "agent": null }
]
}
}
```
- `check` is `null` or absent on a message the guard didn't check: a human's message, a free-text reply,
a notice, or a message from before 217.
- `check.citations` has one entry per entry of the body's `citations`, in the same order.
- `scope` is one of `canon`, `playground`, `supporting` and `web`. A url cite is `web`.
- `agent` is the `Mosaic-Agent` trailer value, `<business>/<instance>`, from
`git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>`, or `null` when the
file's last commit at `commit` carries none. A url cite has `agent: null`. The Console shows the
agent-written label when `agent` is a nonempty string, and names the agent.
The Console trusts nothing in the body for scope or label. A cite with no matching check entry renders
with no badge and no label. It doesn't fall back to a scope it worked out itself.
If 217 uses other names, I change mine to theirs before either of us pushes (sage-3591 item 1).
## A2. The revert verb I assume (to 288, Honker)
- Verb: `library.revert`. A human verb (design M4): from an agent session it refuses `human-required`,
like `library.status` (`broker.mjs:5133`).
- Args: `{ "project": <business project name>, "commit": <40 lowercase hex> }`. The design writes
`library.revert {commit}`. `project` is added to match the other library verbs, and like theirs it is the
key of `business.projects` (`mirror.mjs:31`), which is the chat project's `repo` link, not its UUID. Both are required, and no other key
is allowed (`invalid-request`), as the other library verbs check with `keys(args, ...)`.
- Result: `{ "project", "path", "reverted": <the commit given>, "commit": <the new commit> }`.
- Refusal codes the Console shows by name:
- `library-revert-not-agent`: `commit` has no `Mosaic-Agent` trailer for this business, or isn't
reachable from the served commit.
- `stale-base`: a later commit changed that path.
- `human-required`, `invalid-request`, `project-not-configured`.
- Before 288 lands, the bus refuses the verb (expected `unknown-verb`, `broker.mjs:6578`), and the Console
shows that code.
- HTTP status in the Console (`packages/webui/src/bus.mjs` `busStatus`): `stale-base` and
`library-revert-not-agent` map to 409. Every other code keeps the existing mapping.
## A3. The status line
`GET /api/library/status?project=<id>` proxies `library.status {project}` through the existing
`libraryStatus` option. `cli.mjs:110` already passes that option, but `startServer` doesn't read it.
The status line shows:
- the served commit (7 characters) and its age;
- `library-diverged` when the sync state names it;
- the fetch error code;
- the reply limits in force, and the keys that fell back;
- the count of R-problems, linking to the list.
It reads the fields `readSyncState` returns at the base (`packages/library/src/mirror.mjs:232`). It shows
any field 217 adds (the limits) only when present.
## A4. "Changed since" (ruled sage-3596)
Sage ruled that 217 supplies `last`. On a `thread.messages` row that carries `check`, each path cite's
entry gets `last`, the file's last commit at the served tip, read when the message is read. When `last`
is absent the Console marks nothing changed, and a test pins that.
One more field is needed, raised in morgana-0200. The cited `commit` is a library commit, not the
file's own last commit, so a cite made at a commit that didn't touch the file would differ from `last`
while the file is unchanged. I asked 217 to also store `file`, the file's last commit at the cited
commit. That's `%H` from the same `git log -1 <commit> -- <path>` call that reads the trailer, so it
costs no extra git call. The Console marks a cite changed when both `file` and `last` are 40-hex and
differ. The cited commit is reachable from the served one (5.2), so `file` is an ancestor of `last`,
and differing means newer. A missing or malformed `file` or `last` marks nothing.
The entry the Console reads, in full: `{ scope, agent, file?, last? }`.
## A5. `no-cited-answer` retires
`renderItems` and its `NOTICES` (`no-cited-answer`, `no-answer`, `turn-failed`) go. 217 sends the
runner's fixed texts as plain bodies (design 5.2, "Runner texts"), and renderReply shows those as free
text. Design 7.4 names no other notice than `library-write`, so nothing is lost (sage-3596 item 3).
## Files
Both deviations below were accepted in sage-3596 and are named in the manifest for Dewey. The brief owns these:
- `packages/library/src/render.mjs`: `renderReply` replaces `renderItems`.
- `packages/library/src/index.mjs`: the export.
- `packages/library/README.md`: the section.
- `packages/library/tests/render.test.mjs`.
It also owns `bus.js`, its stylesheet, `serve.mjs` (only the two routes) and new webui tests. Two
deviations, both recorded here:
- The chat view's message list is `packages/webui/src/public/chat.js:85` (`window.MosaicChat`), not
`bus.js`. The reply, notice and Revert rendering goes in `chat.js` and `chat.css`. The status line
goes in the chat thread view, so it is also in `chat.js`.
- `packages/webui/src/console-bus.mjs` and `cli.mjs` each get one `libraryRevert` line, beside the
existing `libraryStatus` wiring.
## Overlap with row 220 (C2-10a, Researcher, 3ef6da90d)
220 changes one line each in the webui tests discord.test.mjs, fixture.mjs, history-fixture.mjs,
relaunch.test.mjs and return-flow.test.mjs. Its other paths are in control-board, conversation, discord
and business. 225 doesn't touch those five files or those packages. Its webui tests are new files.
## Handoff to Fethry (mos-1501, Q245)
The row moved to Fethry (Codex, fethry/row225). Everything below is **untested**: no suite or single file
has run on it, since a run needs Sage's CLEAR. Each changed file passes `node --check`.
Done in the handoff commit:
- `render.mjs` `renderReply(body, {check, changed, name})`; index export; README section;
`render.test.mjs` rewritten. `renderItems` is gone from every package.
- `serve.mjs`: `/render.mjs` static entry (owner and user file maps), `GET /api/library/status`
(one `project` query, answer must name that project), `POST /api/library/revert` (save's boundary,
strict `{project, commit}`, refusal shows the code, lost or unusable answer is `outcome-unknown`,
never retried). Both are nulled for a granted user, and `userRoute` keeps them owner-only. The tester
listener refuses `libraryRevert`.
- `bus.mjs`: `library-revert-not-agent: 409`. That re-pins `packages/webui/src/bus.mjs` in
`packages/bus/tests/channel-proof.test.mjs`, with a comment line, as row 254 did. It's a third deviation,
in another package's test.
- `console-bus.mjs` and `cli.mjs`: `libraryRevert`, beside `libraryStatus`.
- `chat.js`: dynamic `import('/render.mjs')` (CSP is `script-src 'self'`); body text until it loads.
Cites with a scope badge, the agent-written label, "changed since" from `file`/`last`, links only for
https with `rel="noopener noreferrer"`. A notice gets a Revert button behind `confirm()`. The library
status line goes under the thread heading.
- `chat.css`: cite, badge, notice and status styles.
- New tests: `packages/webui/tests/library-routes.test.mjs` and
`packages/webui/tests/chat-replies-browser.test.mjs`. 220's five files are untouched.
Decisions made in the build:
- The library name is the chat project's `repo`. A project with no repo reads no status and its
Revert does nothing.
- These codes hide the status line: `project-not-declared`, `library-mapping-invalid`,
`project-not-configured`, `not-configured` and `owner-only`.
- Otherwise every code-repo thread, and every granted user, would show "unavailable".
- Cost: a broken library mapping also hides the line. `library.status` from the CLI still shows it.
- "Age" is the time since `lastFetch` ("fetched 5 min ago"), not the served commit's age, which the
status doesn't carry.
- The limits fields are assumed to be `replyLimits {requireCitation, citationsPerReply}` and
`replyLimitsFallback [keys]`, and are shown only when present. **Not agreed with 217.** Confirm with
Beakley and rename.
- A notice body's shape is all the renderer checks. An agent in a business without a library could
send a body that looks like a notice. The page would show it, but `library.revert` still refuses it.
Left:
1. Get CLEAR and run the webui and library suites, `channel-proof.test.mjs`, and the two new files whole.
The browser test's selectors and waits have never run.
2. Agree the limits field names with 217 (Beakley).
3. Granted-user R6 G5 (`granted-user.test.mjs:252`) lists owner verbs by hand. Add `library.revert`
once 288 lands.
4. BUILD.md: the mutant list and the mos-1275 timeout plan.
5. The manifest names the three deviations: chat.js and chat.css in place of bus.js, the console-bus and
cli lines, and the channel-proof re-pin.
@@ -0,0 +1,87 @@
# Row 296: FED R7, scratch two-instance acceptance (#1802)
## Start
- Started 2026-10-23 18:53:27Z, on Sage's sage-3583 (mos-1489).
- Base: origin/refactor 45dca69f6 (pinned; local copy used, no fetch: fetch runs the credential node script, which
is held during Darkwing's re-pin window).
- Branch: quackerjack/row296 in worktree quackerjack-fed-r5-row296.
- Brief: docs/plans/2026-10-23_mvp-readiness-rows.md at 45dca69f6, section "Brief: FED R7, scratch two-instance
acceptance" (line 77).
- Constraints from Sage: scratch roots only (mktemp -d under /mnt/storage/scratch/tmp), never ~/.mosaic-dev or
the live bus. No push, claim, queue write, node or test run until CLEAR.
## What the brief asks for (summary)
- One script test under scripts/ with its harness in packages/*/tests/, as row 190's tests are.
- Two configs, two data roots, two Consoles, one fake OP. Consoles start outside agent ancestry in a transient
user unit. They never use the live stack's units, ports or sockets. The script removes its units on exit (trap).
- Test (design section 7 R7 gate): Jason's test identity, granted collaborator on one hub project, signs in,
makes a thread, sends to the granted instance, gets the reply in the thread, is refused on another project and
on board reads, is revoked, is refused again, and the run ends. The factory shows nothing of the hub.
- Mutants: grant project list widened to `*`; revocation claim end skipped.
- Gate: Filbert approves the manifest; the script test and every scripts/test-*.sh green on Sage's gate; no unit,
port or socket of the script outlives the run. R8 (live window) stays row 209.
## Open reads before design
- R4 and R6 candidates (the brief says it builds on their combined candidate). Confirm what they expose: the
hub sign-in, grant and revocation paths, and the Console's board-read refusal.
- Row 190's test harness (several instances on one uid): how it starts units and cleans up.
- Transient user unit support on this host (systemd-run --user), and how the existing tests detect it.
## Status
- 18:53Z: worktree created, brief read. No code yet.
## Handoff 2026-10-23 19:10:09Z (Mos ruling Q245, mos-1503)
Row 296 moves to Lena (Codex seat, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a, branch lena/row296). Quackerjack
stops here: no builds, no claim, no queue writes. This design is the handoff; Lena continues it.
Base: 45dca69f6. R4 landed as 2efd127ce (ancestor of the base, checked with merge-base --is-ancestor). R6 landed as
d3059c2c3 (row 294). R7 is claimable once Darkwing's window ends (Sage, sage-3592).
What I verified by reading (not by running):
- packages/cli/src/access.mjs:65 reads `system.federation?.peers ?? []` (the R6 seam).
- packages/bus/src/user-cli.mjs exists at base (the user-cli transport).
- Row 190's tests (packages/cli/tests/stack-instances.test.mjs, stack-release.test.mjs T56, stack-release-fixture.mjs)
drive stand-in units and fixtures. They do not start real transient user units. Not a model for R7's Consoles.
- Real transient-unit launch pattern: packages/conversation/src/cohort.mjs (`scopeAvailable()` and `ScopeLauncher`,
systemd-run --user --scope -p Delegate=yes --unit=...; MANAGER_ENV = XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS).
- Test-only spawn of systemd-run in packages/conversation/tests is the nearest precedent for start/stop cleanup.
What I did NOT verify (so treat as open): the hub sign-in, grant, revocation and board-read paths in R4/R6 code; the
Console startup flags; whether the fake OP can be a local stand-in; the trap-based unit cleanup.
Design (proposal, not yet checked against code):
1. scripts/test-federation-acceptance.sh: the entry point. Creates a mktemp -d scratch root under
/mnt/storage/scratch/tmp (never ~/.mosaic-dev, never the live bus). trap removes every unit it started and the
scratch root. Refuses to run if a transient unit name collides.
2. Two configs (instance A = hub-side granted seat, instance B = collaborator's home), each with its own
MOSAIC_CONFIG, data root and federation.self; federation.peers names the other.
3. One fake OP: a local stand-in that issues Jason's test identity. Its port and socket are per-run, not the live ones.
4. Two Consoles, each in its own transient user unit (systemd-run --user --unit=mosaic-r7-<run>-<A|B>); outside agent
ancestry; ports picked per run.
5. Test flow (the R7 gate): identity -> grant on one hub project -> sign in -> thread -> send to granted instance ->
reply in thread -> refused on another project -> refused on board reads -> revoke -> refused after revoke -> run
ends. Factory output must show nothing of the hub.
6. Mutants: (a) grant project list widened to `*`, must fail the other-project refusal; (b) revocation claim end
skipped, must fail the refusal-after-revoke check.
Unresolved decisions for Lena (I did not decide these):
- Whether the harness lives in packages/cli/tests (matching row 190) or packages/conversation/tests (matching
cohort.mjs). Brief says packages/*/tests/; pick the one that owns the Console start path.
- Fake OP: in-process HTTP stand-in versus file-backed identity. No preference recorded.
- Port selection: fixed range per run vs OS-assigned. Brief forbids the live ports; it does not pick a range.
R2 and R5 learning:
- R2 (row 203, #1701) landed as a4da4e6df. I built it. Relevant to R7: validateFederation in
scripts/mosaic-config.mjs rejects unknown keys and bad ids, and test-config.sh has an FED block
(expect_fed_refusal). Use federation.self/peers exactly as row 203 validates them.
Mutant runner pattern: one textual edit that must occur exactly once, then a named test must fail; restore from git.
Name filter gotcha: `node --test --test-name-pattern` skips subtests whose parent does not match; include the
parent name (T57 for row 203).
- R5: I did not build R5 in this session and have no first-hand notes on it. Ask Sage for R5's landing sha and notes.
Status: design written; no code, no test runs, no claim, no push of the test files. Next owner: Lena.
+1 -2
View File
@@ -1593,5 +1593,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
2026-10-23 | Fethry (Codex gpt-6.1-sol, 14f80e3c) | Row 225 C2-10b (#1726), Q245 handoff | Recovered Morgana's 9a131e956 by fast-forward on fethry/row225 in the assigned T3 worktree. Static review and contract confirmation in progress; all Node/tests require Sage CLEAR. No acceptance claim.
2026-10-23 | Fethry (14f80e3c) | Row225 T1 lane A sage-3641/3647 (#1726) | Continued Morgana handoff, fixed nested status limits and wrong-project revert receipt after RED, kept invalid-mapping diagnosis visible with RED/GREEN. Renderer9/0, routes13/0, browser9/0, full library81/0, channel-proof2/0; isolated mutants14/14, restored SHA256 checks and clean worktree sources. Old-test approval mos1510 preserved80/66 and3/1. Slot returned with no processes around19:29Z. Dewey source-review packet prepared; full webui/build and integration gate remain pending.
- 2026-10-23 — lena (Codex, db27faca): row 296 / #1802 FED R7; recovered Quackerjack design on lena/row296, inspecting acceptance paths; Node/tests held pending Sage CLEAR, no live changes.
+1 -3
View File
@@ -9,8 +9,6 @@
// Row 252 (#1753, Webby N5) re-pins human.mjs, whose readProcess also keeps the five
// MOSAIC_BUS_CONSOLE_* keys for the Console wrapper's proof, and webui bus.mjs for consoleCall,
// which shares humanCall's spawn as transport(). The human proof and its spawn are unchanged.
// Row 225 (C2-10b) re-pins webui bus.mjs for one STATUS entry, library-revert-not-agent: 409; no proof
// or transport change.
import test from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
@@ -31,7 +29,7 @@ const baselines = {
// Row 194 (#1692, brief item 7, sage-1610 a): organization-tracker-required 400 and
// organization-tracker-bot-taken 409; no proof or transport change.
// Row 301 (#1808, mos-1449): the user transport moved from user-bus.mjs into bus.mjs; humanCall unchanged.
"packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
"packages/webui/src/bus.mjs": "604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c",
};
const repoFile = path => new URL("../../../" + path, import.meta.url);
@@ -0,0 +1,234 @@
// FED R7 / #1802. Run through scripts/test-federation-acceptance.sh: it owns
// the child process groups and scratch lifetime. Ordinary CLI suites skip it.
// sage-3625: federation-host.mjs supplies a labelled readProcess ancestry
// stand-in for setup, grant/revoke and both Console transports. This is NOT
// a genuine outside-agent human proof. Product transports/grant checks stay real.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
import { request } from 'node:http';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { businessDoc, launchDoc, writeJson } from '../../business/tests/helpers.mjs';
import { REPO_SKILLS } from '../../harness/tests/helpers.mjs';
import { DatabaseSync } from 'node:sqlite';
import { SESSION_COOKIE } from '../../webui/src/sso.mjs';
import { fakeOidc } from '../../webui/tests/helpers/fake-oidc.mjs';
import { socketPath, REPO } from '../src/config.mjs';
import { startTimeOf } from '../src/host.mjs';
import { launchLogFile, readSessions } from '../../seat/src/session.mjs';
const root = process.env.MOSAIC_R7_ROOT;
const helper = fileURLToPath(new URL('./fixtures/federation-human.mjs', import.meta.url));
const adapterModule = fileURLToPath(new URL('./fixtures/thread-adapter.mjs', import.meta.url));
const webCli = join(REPO, 'packages/webui/src/cli.mjs');
const cli = join(REPO, 'packages/cli/src/cli.mjs');
const pause = ms => new Promise(r => setTimeout(r, ms));
const json = path => JSON.parse(readFileSync(path, 'utf8'));
function run(cmd, args, options = {}) {
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 30000, ...options });
assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr ?? ''} ${r.error?.message ?? ''}`);
return r.stdout;
}
async function until(read, label, ms = 20000) {
const end = Date.now() + ms;
while (Date.now() < end) {
const value = await read();
if (value) return value;
await pause(50);
}
assert.fail(`R7 assertion: ${label}`);
}
function http(instance, path, { cookie, body, method = body === undefined ? 'GET' : 'POST' } = {}) {
return new Promise((resolve, reject) => {
const text = body === undefined ? undefined : JSON.stringify(body);
const headers = { host: new URL(instance.origin).host };
if (cookie) headers.cookie = cookie;
if (text !== undefined) Object.assign(headers, { origin: instance.origin, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json', 'content-length': Buffer.byteLength(text) });
const req = request({ host: '127.0.0.1', port: instance.port, path, method, headers, timeout: 5000 }, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data && String(res.headers['content-type']).startsWith('application/json') ? JSON.parse(data) : data }));
});
req.on('error', reject);
req.on('timeout', () => req.destroy(new Error('R7 HTTP timeout')));
req.end(text);
});
}
let serial = 0;
function child(instance, label, command) {
const log = join(root, `${instance.name}-${label}.log`);
const fd = openSync(log, 'a', 0o600);
let proc;
try { proc = spawn(command[0], command.slice(1), { detached: true, env: instance.env, stdio: ['ignore', fd, fd] }); }
finally { closeSync(fd); }
proc.on('error', e => { console.error(`R7 child ${label}: ${e.message}`); });
assert.ok(proc.pid, `R7 ${label} spawned`);
const start = startTimeOf(proc.pid);
assert.ok(start, `R7 ${label} start time recorded`);
// EXIT trap owns the detached group even if Node's test driver is killed.
writeFileSync(join(root, 'groups'), `${proc.pid} ${start}\n`, { flag: 'a' });
proc.unref();
return { log, proc };
}
// Jason-shell ancestry stand-in is in the fixture host; transport/path are real.
function human(instance, verb, args = {}) {
const out = join(root, `human-${++serial}.json`);
run(process.execPath, [helper, socketPath(instance.dataRoot), instance.name, verb, JSON.stringify(args), out], { env: instance.env });
return json(out);
}
// R5/R4 CLI with that same labelled fixture proof, never a live human shell.
function access(instance, args) {
return run(process.execPath, [cli, 'access', ...args, '--business', instance.name], { env: instance.env });
}
function prepare(name, iss, ca) {
const home = process.env.HOME;
const env = { HOME: home, PATH: process.env.PATH, TMPDIR: process.env.TMPDIR };
run(join(REPO, 'scripts/bootstrap.sh'), ['--instance', name], { env });
const config = join(home, '.config', `mosaic-${name}`, 'config.json');
const system = json(config), dataRoot = system.dataRoot, configDir = dirname(config);
const fixtureRoot = join(root, name);
mkdirSync(fixtureRoot, { recursive: true });
const doc = businessDoc(fixtureRoot, name);
doc.human = 'melanie';
doc.plugins = { harness: ['pi'], channel: ['webui'] };
doc.roles.chief.vars = { harness: 'pi', model: 'claude-opus-5-5', thinking: 'high' };
doc.roles.assistant = { definition: 'assistant', vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
doc.launch = launchDoc('chief', ['coder', 'assistant'], { opus: 1, sonnet: 2 });
doc.launch.threadIdleSeconds = 3600;
for (const role of Object.values(doc.roles)) role.vars = { harness: 'pi', model: 'glm-5.3-flash', thinking: 'high', ...role.vars };
writeJson(join(configDir, 'businesses', `${name}.json`), doc);
const origin = `https://${name}.example.test`;
system.console = { sso: { publicOrigin: origin, providers: [{ id: 'test-op', issuer: iss.issuer, clientId: iss.clientId,
clientSecret: { slot: name }, algs: ['RS256'], subjects: [{ sub: 'owner-sub', name: 'melanie' }] }] } };
system.federation = { self: name, peers: [{ id: name === 'hub' ? 'factory' : 'hub', origin: `https://${name === 'hub' ? 'factory' : 'hub'}.example.test`, label: name === 'hub' ? 'Factory' : 'Hub' }] };
writeJson(config, system);
writeJson(join(configDir, 'access.json'), { accessVersion: 1, revision: 1, users: [], grants: [] });
// Generated fake OP material only; never copies a host slot or credential.
writeJson(join(configDir, 'secrets/mosaic-stack', name), iss.clientSecret);
cpSync(REPO_SKILLS, join(dataRoot, 'skills-enabled'), { recursive: true });
const projectRoot = doc.projects.stack.root;
const gitEnv = { ...env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' };
const git = (...args) => run('git', ['-c', 'user.name=R7 fixture', '-c', '[email protected]', '-C', projectRoot, ...args], { env: gitEnv });
git('init', '-q', '-b', 'main');
writeFileSync(join(projectRoot, 'file.txt'), `${name} fixture\n`);
git('add', 'file.txt'); git('commit', '-q', '-m', 'scratch fixture');
const adapter = join(fixtureRoot, 'adapter.sh');
writeFileSync(adapter, `exec '${process.execPath}' '${adapterModule}'\n`);
return { name, origin, configDir, dataRoot, adapter, env: { ...env, MOSAIC_CONFIG: config, NODE_EXTRA_CA_CERTS: ca }, port: null };
}
async function signIn(instance, op, iss, sub) {
const start = await http(instance, '/auth/login');
assert.equal(start.status, 303, JSON.stringify(start.body));
const location = new URL(start.headers.location);
const { code } = op.grant(iss, location.href, { sub });
const cookie = start.headers['set-cookie'][0].split(';')[0];
const result = await http(instance, `/auth/callback?${new URLSearchParams({ code, state: location.searchParams.get('state') })}`, { cookie });
const session = (result.headers['set-cookie'] ?? []).find(c => c.startsWith(`${SESSION_COOKIE}=`) && !c.includes('Max-Age=0'))?.split(';')[0];
return { ...result, cookie: session };
}
const logs = i => existsSync(launchLogFile(i.dataRoot, i.name)) ? readFileSync(launchLogFile(i.dataRoot, i.name), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : [];
function claimHistory(instance, run) {
const store = new DatabaseSync(join(instance.dataRoot, 'bus/bus.sqlite'), { readOnly: true });
try { return store.prepare('SELECT op, reason FROM thread_claims WHERE holder_run=? ORDER BY seq').all(run); }
finally { store.close(); }
}
test('FED R7 scratch two-instance acceptance', { skip: !root && 'run scripts/test-federation-acceptance.sh', timeout: 180000 }, async t => {
const op = await fakeOidc();
t.after(() => op.close());
const hubIssuer = op.issuer('hub'), factoryIssuer = op.issuer('factory');
assert.notEqual(hubIssuer.clientId, factoryIssuer.clientId);
const ca = join(root, 'fake-op-ca.pem'); writeFileSync(ca, op.cert);
assert.ok(![7330, 7331, 7332].includes(Number(new URL(op.base).port)));
writeFileSync(join(root, 'ports'), `${new URL(op.base).port}\n`, { flag: 'a' });
const hub = prepare('hub', hubIssuer, ca), factory = prepare('factory', factoryIssuer, ca);
assert.notEqual(hub.dataRoot, factory.dataRoot);
assert.notEqual(hub.env.MOSAIC_CONFIG, factory.env.MOSAIC_CONFIG);
for (const i of [hub, factory]) {
const ready = join(root, `${i.name}-host.ready`);
const host = child(i, 'host', [process.execPath, '--experimental-test-module-mocks',
fileURLToPath(new URL('./fixtures/federation-host.mjs', import.meta.url)), i.name, i.adapter, ready]);
await until(() => existsSync(ready), `${i.name} fixture host ready; ${host.log}`);
writeFileSync(join(root, 'sockets'), `${socketPath(i.dataRoot)}\n`, { flag: 'a' });
}
const shared = human(hub, 'project.create', { name: 'hub-shared', repo: 'stack' }).id;
const privateProject = human(hub, 'project.create', { name: 'hub-private' }).id;
human(factory, 'project.create', { name: 'factory-only' });
access(hub, ['user', 'add', 'jason-test', '--home', 'factory', '--subject', 'test-op:jason-test-sub', '--revision', '1']);
access(hub, ['grant', '--user', 'jason-test', '--projects', shared, '--instances', 'assistant', '--level', 'collaborator', '--revision', '2']);
for (const i of [hub, factory]) {
const { log } = child(i, 'console', [process.execPath, webCli, 'serve', '--port', '0', '--business', i.name, '--board', 'http://127.0.0.1:1', '--sso', '--send']);
i.port = Number(await until(() => existsSync(log) && readFileSync(log, 'utf8').match(/Mosaic Console: http:\/\/127\.0\.0\.1:(\d+)\//)?.[1], `${i.name} Console started`));
assert.ok(![7330, 7331, 7332].includes(i.port));
writeFileSync(join(root, 'ports'), `${i.port}\n`, { flag: 'a' });
}
assert.notEqual(hub.port, factory.port);
if (process.env.MOSAIC_R7_INTERRUPT === '1') {
console.log('R7 interruption: both Consoles started; killing test driver');
process.kill(process.pid, 'SIGKILL');
await pause(60000);
}
const signed = await signIn(hub, op, hubIssuer, 'jason-test-sub');
assert.equal(signed.status, 303, JSON.stringify(signed.body)); assert.ok(signed.cookie);
const cookie = signed.cookie;
const config = await http(hub, '/api/config', { cookie });
assert.equal(config.status, 200); assert.equal(config.body.user, true);
const projects = await http(hub, '/api/chat/projects', { cookie });
assert.equal(projects.status, 200, JSON.stringify(projects.body));
assert.deepEqual(projects.body.rows.map(p => p.id), [shared], 'R7 ungranted project must not appear');
const created = await http(hub, '/api/chat/threads', { cookie, body: { project: shared, instance: 'assistant', name: 'Jason scratch thread' } });
assert.equal(created.status, 200, JSON.stringify(created.body));
const thread = created.body.id;
const sent = await http(hub, `/api/chat/threads/${thread}/messages`, { cookie, body: { body: 'R7 scratch question' } });
assert.equal(sent.status, 200, JSON.stringify(sent.body));
const reply = await until(async () => {
const r = await http(hub, `/api/chat/threads/${thread}/messages`, { cookie });
assert.equal(r.status, 200, JSON.stringify(r.body));
return r.body.rows.find(m => m.class === 'RESULT' && m.in_reply_to === sent.body.id);
}, 'granted instance replies in Jason thread');
assert.equal(reply.from_role, 'assistant'); assert.equal(reply.to_role, 'user');
assert.match(reply.body, /R7 scratch question/);
const refused = await http(hub, '/api/chat/threads', { cookie, body: { project: privateProject, instance: 'assistant', name: 'must refuse' } });
assert.equal(refused.status, 403); assert.equal(refused.body.error, 'access-refused');
assert.deepEqual(json(join(hub.configDir, 'access.json')).grants[0].projects, [shared], 'R7 grant must name exactly one project');
for (const path of ['/api/board', '/api/bus/inbox', '/api/bus/tasks']) {
const r = await http(hub, path, { cookie });
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'owner-only', path);
}
const factoryLogin = await signIn(factory, op, factoryIssuer, 'jason-test-sub');
assert.equal(factoryLogin.status, 403); assert.equal(factoryLogin.cookie, undefined);
const factoryOwner = await signIn(factory, op, factoryIssuer, 'owner-sub');
assert.equal(factoryOwner.status, 303); assert.ok(factoryOwner.cookie);
const factoryProjects = await http(factory, '/api/chat/projects', { cookie: factoryOwner.cookie });
assert.equal(factoryProjects.status, 200);
assert.deepEqual(factoryProjects.body.rows.map(p => p.name), ['factory-only']);
const factoryThreads = await http(factory, '/api/chat/threads', { cookie: factoryOwner.cookie });
assert.equal(factoryThreads.status, 200); assert.deepEqual(factoryThreads.body.rows, []);
const hubCookieAtFactory = await http(factory, '/api/chat/projects', { cookie });
assert.equal(hubCookieAtFactory.status, 401);
const [launch] = logs(hub).filter(e => e.event === 'launch' && e.thread === thread);
assert.ok(launch, 'R7 sandboxed run launched');
const liveRun = readSessions(hub.dataRoot).find(s => s.run === launch.run);
assert.ok(liveRun, 'R7 run remains live before revoke');
access(hub, ['revoke', '--user', 'jason-test', '--revision', '3']);
const after = await http(hub, '/api/chat/projects', { cookie });
assert.equal(after.status, 401); assert.equal(after.body.error, 'sign-in required');
assert.equal((await signIn(hub, op, hubIssuer, 'jason-test-sub')).status, 403);
// Pin the claim before waiting for process exit: a missing broker revocation
// must fail this assertion, even if it also prevents the runner from ending.
assert.ok(claimHistory(hub, launch.run).some(r => r.op === 'revoke' && r.reason === 'access-revoked'),
'R7 revoke must end the claim as access-revoked');
const ended = await until(() => logs(hub).find(e => e.event === 'end' && e.run === launch.run), 'revoked run ended');
assert.equal(ended.exitCode, 0, JSON.stringify(ended));
assert.deepEqual(readSessions(hub.dataRoot), []);
await until(() => startTimeOf(liveRun.pid) !== liveRun.startTime && startTimeOf(liveRun.runnerPid) !== liveRun.runnerStartTime, 'revoked engine and runner processes gone');
assert.equal(claimHistory(hub, launch.run).at(-1).op, 'revoke', 'R7 shutdown must not replace missing revocation with release');
assert.deepEqual(human(factory, 'thread.list'), []);
});
+67
View File
@@ -0,0 +1,67 @@
// FED R7 fixture, sage-3625: the ancestry walk is a stand-in for Jason's
// shell. Keep this override in a test-only process, never in product code.
// As channel-fixture.mjs, real pid/uid/argv/startTime/nonce checks remain;
// verifyHuman sees ppid=1 instead of walking the development agent's tree.
import { mock } from 'node:test';
const humanUrl = new URL('../../../bus/src/human.mjs', import.meta.url).href;
const real = await import(humanUrl);
mock.module(humanUrl, { namedExports: {
readProcess: real.readProcess,
verifyHuman: (proof, options) => real.verifyHuman(proof, {
...options, readProcess: pid => ({ ...real.readProcess(pid), ppid: 1 }),
}),
} });
const { startBroker } = await import('../../../bus/src/runtime.mjs');
const { BusError } = await import('../../../bus/src/broker.mjs');
const { loadSystem, bootConfig } = await import('../../src/config.mjs');
const { createLauncher } = await import('../../src/launcher.mjs');
const { fakeHarnesses } = await import('../helpers.mjs');
const { writeFileSync } = await import('node:fs');
const [business, adapter, ready] = process.argv.slice(2);
const system = loadSystem();
const runtime = await startBroker(bootConfig({ system, businessId: business }));
const hooks = [], cleanups = [];
const ops = {
identity: m => runtime.identity(m.cap),
authorizeLaunch: m => runtime.authorizeLaunch(m.cap, m.instance),
refuse: m => runtime.refuse(m.cap, m.code, m.subject),
endLaunch: m => runtime.endLaunch(m.record),
credentialStatus: m => runtime.credentialStatus(m.business, m.role),
threadsState: m => runtime.threadsState(m.business),
threadIdle: m => runtime.threadIdle(m.record),
mayLaunch: m => runtime.mayLaunch(m),
landHeld: m => runtime.landHeld(m.record),
landHolders: m => runtime.landHolders(m.business),
landRelease: m => runtime.landRelease(m.record),
sessionClears: m => runtime.sessionClears(m.business),
lifecycleRequests: m => runtime.lifecycleRequests(m.business),
};
const wrap = fn => async value => {
try { return fn(value); }
catch (e) { throw Object.assign(new Error(e.message), { code: e instanceof BusError ? e.code : 'fixture-host-refused' }); }
};
const host = {
business, path: runtime.path, dataRoot: system.dataRoot,
op: wrap(m => { if (!Object.hasOwn(ops, m.op)) throw new BusError('invalid-host-request'); return ops[m.op](m); }),
bindLaunch: wrap(record => runtime.bindLaunch(record)),
beforeClose: fn => void hooks.push(fn),
};
const launcher = createLauncher({ host, system,
makeLoader: await fakeHarnesses({ after: fn => cleanups.push(fn) }, { pi: adapter }, write => write('channel', 'webui',
`export * from ${JSON.stringify(new URL('../../../plugins/channel/webui/index.mjs', import.meta.url).href)};`)),
readOnly: [new URL('./thread-adapter.mjs', import.meta.url).pathname],
harnessVersion: '0.85.1', sessionDefaults: { pollInterval: 600000 }, threadTickMs: 100,
});
let closing = false;
async function close() {
if (closing) return;
closing = true;
for (const fn of hooks) await fn(0);
await runtime.close();
for (const fn of cleanups) await fn();
process.exit(0);
}
process.once('SIGTERM', close);
process.once('SIGINT', close);
await launcher.listen();
writeFileSync(ready, 'ready\n', { mode: 0o600 });
+7
View File
@@ -0,0 +1,7 @@
// R7 fixture: actual human-cli path with the fixture host's labelled ancestry override.
// This stands in for Jason's shell (sage-3625); it does not prove real human ancestry.
import { writeFileSync } from 'node:fs';
import { humanTransport } from '../../src/transport.mjs';
const [socket, business, verb, args, out] = process.argv.slice(2);
const result = await humanTransport({ socket, business })(verb, JSON.parse(args));
writeFileSync(out, JSON.stringify(result), { mode: 0o600 });
+10 -17
View File
@@ -104,28 +104,21 @@ in an extension other than `.md`, in any case, embeds an attachment, and it is n
a dot and up to 8 letters and digits, at least one a letter. Any other unresolved embed is R4, and so is
an unresolved front matter value written as an embed (C2-01b N3, sage-1900).
## renderReply
## renderItems
`renderReply(body, { check, changed, name })` turns a stored reply into what both channels show. It
replaces C2-01's items renderer and is pure. A body is free text, or the design 5.2 object
`{ "text", "citations" }` with library cites `{ path, commit, lines, quote, as? }` and web cites
`{ url, quote }`. Any other body, a JSON object with another key or a bad cite included, renders as
free text: the whole body, with no citations.
`check` is the stored check result (C2-06), `{ citations: [{ scope, agent }] }` in body order. A path
cite's badge (`canon`, `playground` or `supporting`) and its agent-written label come only from it. A
url cite is always `web`, and is a link only when its scheme is `https`. `changed({ path, commit }, i)`
says whether the file's last commit is newer than the cited one. `plain` is the rendering as text:
`renderItems(body, { scopeOf, changed })` turns a reply body into the plain text both channels show.
It is pure. Item kinds are `fact`, `finding`, `correction` (with `from` and `to`), `question` and
`idea`. Each cite is a path with commit, line range and quote, or a url with a quote.
```
Mara's eyes are grey in chapter 3.
[canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"
[supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)
[web] https://example.invalid/tides "spring tides"
1. Correction: Chapter 1 says grey.
From: "green eyes"
To: "grey eyes"
[canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes" (changed since)
```
The broker's write notice, `{ "notice": "library-write", op, path, commit }`, gives the line
`<name> wrote <path> @<commit>` or `<name> deleted <path> @<commit>`.
A body of `{ "items": [], "notice": ... }` gives `No cited answer`, `No answer` or `The turn failed`.
Any other shape gives the fixed line `This reply could not be shown.` and none of the body's text.
## Survey
+1 -1
View File
@@ -4,7 +4,7 @@ export { checkMapping, TYPES } from './mapping.mjs';
export { classify } from './scope.mjs';
export { extractLinks, parseWikilink, resolver, formOf, isAttachment } from './links.mjs';
export { readLibrary, listTree, readTexts, parseFile, parseFrontMatter, headCommit, MAX_BLOB, LINK_FIELDS } from './reader.mjs';
export { renderReply, SCOPES, NOTICE_OPS } from './render.mjs';
export { renderItems, KINDS, NOTICES, UNREADABLE } from './render.mjs';
export { survey, formatSurvey, surveyMain } from './survey.mjs';
export { SYNC_STATE_FILE, mirrorPath, ensureMirror, withLibraryLock, servedCommit, readSyncState, libraryStatus, readLibraryFile } from './mirror.mjs';
export { syncCommand, sync, ASKPASS, SLOT, SYNC_INTERVAL_MS } from './sync.mjs';
+49 -62
View File
@@ -1,72 +1,59 @@
// renderReply(body): how a stored reply looks (design 7.4), shared by the Console chat (C2-10b) and the
// Discord relay (C2-18). It replaces C2-01's items renderer. Pure: no git, no clock, no DOM. The caller
// passes what only the bus knows: the stored check result (C2-06) and whether a cited file changed.
// The guard checks a reply before it is stored. This only decides how a body reads; it trusts nothing
// in the body for a scope or the agent-written label.
export const SCOPES = Object.freeze(['canon', 'playground', 'supporting', 'web']);
export const NOTICE_OPS = Object.freeze({ write: 'wrote', delete: 'deleted' });
// renderItems(body): how a stored cited reply looks (design 7.4), shared by the Console chat (C2-10b)
// and the Discord relay (C2-18). Pure: no git, no clock. The caller passes what it knows about cites.
// The guard (C2-06) checks limits before a reply is stored. This only refuses to show a body that
// doesn't have the reply shape, and then it shows a fixed line, never the body's text.
export const KINDS = Object.freeze({ fact: 'Fact', finding: 'Finding', correction: 'Correction', question: 'Question', idea: 'Idea' });
export const NOTICES = Object.freeze({ 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' });
export const UNREADABLE = 'This reply could not be shown.';
const SHA = /^[0-9a-f]{40}$/;
const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const line = n => Number.isSafeInteger(n) && n > 0;
const only = (value, keys) => Object.keys(value).every(k => keys.includes(k));
const quoted = text => JSON.stringify(text);
// webby-0041 N5: only an https url is ever a link. Anything else, javascript: and http: included, is text.
function https(url) {
try { return new URL(url).protocol === 'https:' ? url : null; } catch { return null; }
}
// The design 5.2 cite shapes. null when the value isn't one, and then the body is not a cited reply.
function cite(value) {
function cite(value, { scopeOf, changed }) {
if (!plain(value) || typeof value.quote !== 'string') return null;
if (Object.hasOwn(value, 'url')) return typeof value.url === 'string' && only(value, ['url', 'quote']) ? { url: value.url, quote: value.quote } : null;
if (Object.hasOwn(value, 'url')) {
if (typeof value.url !== 'string' || Object.hasOwn(value, 'path')) return null;
return { badge: 'web', url: value.url, ref: value.url, quote: value.quote, changed: false };
}
const { path, commit, lines } = value;
if (!only(value, ['path', 'commit', 'lines', 'quote', 'as']) || typeof path !== 'string' || !SHA.test(commit ?? '')
|| !Array.isArray(lines) || lines.length !== 2 || !line(lines[0]) || !line(lines[1]) || lines[1] < lines[0]
|| (Object.hasOwn(value, 'as') && value.as !== 'canon')) return null;
return { path, commit, lines: [...lines], quote: value.quote, canon: value.as === 'canon' };
if (typeof path !== 'string' || !SHA.test(commit ?? '') || !Array.isArray(lines) || lines.length !== 2
|| !line(lines[0]) || !line(lines[1]) || lines[1] < lines[0]) return null;
const badge = scopeOf?.(path);
const ref = `${path}:${lines[0] === lines[1] ? lines[0] : `${lines[0]}-${lines[1]}`} @${commit.slice(0, 7)}`;
return { badge: badge === 'canon' || badge === 'playground' ? badge : null, path, commit, lines: [...lines], ref,
quote: value.quote, changed: changed?.({ path, commit }) === true };
}
const quoted = text => JSON.stringify(text);
// check: the stored entry for this cite, { scope, agent }, or undefined. A path cite takes its badge only
// from it; a url cite is web by its shape. agent is the Mosaic-Agent trailer value, never the body's.
function shown(c, check, changed, i) {
if (c.url !== undefined) return { badge: 'web', url: c.url, link: https(c.url), ref: c.url, quote: c.quote, agent: null, changed: false };
const scope = plain(check) && SCOPES.includes(check.scope) && check.scope !== 'web' ? check.scope : null;
const agent = plain(check) && typeof check.agent === 'string' && check.agent !== '' ? check.agent : null;
const ref = `${c.path}:${c.lines[0] === c.lines[1] ? c.lines[0] : `${c.lines[0]}-${c.lines[1]}`} @${c.commit.slice(0, 7)}`;
return { badge: scope, path: c.path, commit: c.commit, lines: c.lines, ref, quote: c.quote, canon: c.canon, agent,
changed: changed?.({ path: c.path, commit: c.commit }, i) === true };
}
// The broker's write notice (design, writes): {"notice": "library-write", op, path, commit}, no agent text.
function notice(value, name) {
if (!plain(value) || value.notice !== 'library-write' || !only(value, ['notice', 'op', 'path', 'commit'])
|| !Object.hasOwn(NOTICE_OPS, value.op) || typeof value.path !== 'string' || !SHA.test(value.commit ?? '')) return null;
const who = typeof name === 'string' && name !== '' ? name : 'An agent';
return { kind: 'notice', op: value.op, path: value.path, commit: value.commit,
text: `${who} ${NOTICE_OPS[value.op]} ${value.path} @${value.commit.slice(0, 7)}` };
}
// body: the stored message body, a string (or an already parsed object).
// options.check: the stored check result, { citations: [{ scope, agent }] } in body order, or null.
// options.changed({path, commit}, index) -> true when the file's last commit is newer than the cited one.
// options.name: the sender's name for a notice line.
// Returns { kind: 'text', text, plain } | { kind: 'reply', text, citations, plain } | { kind: 'notice', op, path, commit, text, plain }.
// plain is the whole rendering as text, for a channel that shows no markup.
export function renderReply(body, options = {}) {
const raw = typeof body === 'string' ? body : JSON.stringify(body) ?? String(body);
// body: the stored message body (a JSON string or the parsed object).
// options.scopeOf(path) -> 'canon' | 'playground' | undefined, for the scope badge.
// options.changed({path, commit}) -> true when the file's last commit is newer than the cited one.
// Returns { kind: 'items', items, text } | { kind: 'notice', notice, text } | { kind: 'unreadable', text }.
export function renderItems(body, options = {}) {
const unreadable = { kind: 'unreadable', text: UNREADABLE };
let value = body;
if (typeof body === 'string') { try { value = JSON.parse(body); } catch { value = undefined; } }
const n = notice(value, options.name);
if (n) return { ...n, plain: n.text };
// Design 5.2: a JSON object must be {text, citations} and nothing else. Any other body is a reply whose
// text is the whole body and which carries no citation.
const free = { kind: 'text', text: raw, plain: raw };
if (!plain(value) || typeof value.text !== 'string' || !Array.isArray(value.citations) || !only(value, ['text', 'citations'])) return free;
const cites = value.citations.map(cite);
if (cites.some(c => c === null)) return free;
const checks = plain(options.check) && Array.isArray(options.check.citations) ? options.check.citations : [];
const citations = cites.map((c, i) => shown(c, checks[i], options.changed, i));
const lines = citations.map(c => ` ${c.badge ? `[${c.badge}] ` : ''}${c.ref} ${quoted(c.quote)}${c.agent ? ` (agent-written: ${c.agent})` : ''}${c.changed ? ' (changed since)' : ''}`);
return { kind: 'reply', text: value.text, citations, plain: [value.text, ...lines].join('\n') };
if (typeof body === 'string') { try { value = JSON.parse(body); } catch { return unreadable; } }
if (!plain(value) || !Array.isArray(value.items)) return unreadable;
const keys = Object.keys(value);
if (value.items.length === 0) {
if (keys.length !== 2 || !Object.hasOwn(NOTICES, value.notice)) return unreadable;
return { kind: 'notice', notice: value.notice, text: NOTICES[value.notice] };
}
if (keys.length !== 1) return unreadable;
const items = [];
for (const item of value.items) {
if (!plain(item) || !Object.hasOwn(KINDS, item.kind) || typeof item.text !== 'string' || !Array.isArray(item.cites)) return unreadable;
const correction = item.kind === 'correction';
if (correction && (typeof item.from !== 'string' || typeof item.to !== 'string')) return unreadable;
const cites = item.cites.map(c => cite(c, options));
if (cites.some(c => c === null)) return unreadable;
items.push({ kind: item.kind, label: KINDS[item.kind], text: item.text,
...(correction ? { from: item.from, to: item.to } : {}), cites });
}
const text = items.map((item, i) => [
`${i + 1}. ${item.label}: ${item.text}`,
...(item.kind === 'correction' ? [` From: ${quoted(item.from)}`, ` To: ${quoted(item.to)}`] : []),
...item.cites.map(c => ` ${c.badge ? `[${c.badge}] ` : ''}${c.ref} ${quoted(c.quote)}${c.changed ? ' (changed since)' : ''}`),
].join('\n')).join('\n');
return { kind: 'items', items, text };
}
+68 -82
View File
@@ -1,94 +1,80 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderReply } from '../src/index.mjs';
import { renderItems, UNREADABLE } from '../src/index.mjs';
// Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2).
const A = 'a'.repeat(40), B = 'b'.repeat(40);
const cite = (path, lines, quote, commit = A, extra = {}) => ({ path, commit, lines, quote, ...extra });
const web = (url, quote = 'spring tides') => ({ url, quote });
const reply = (text, citations) => JSON.stringify({ text, citations });
const CITED = reply("Mara's eyes are grey in chapter 3.", [
cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', A, { as: 'canon' }),
cite('Book One/Playground/What If.md', [5, 6], 'a storm', B),
cite('Notes/Eyes.md', [9, 10], 'green eyes'),
web('https://example.invalid/tides'),
]);
const CHECK = { citations: [{ scope: 'canon', agent: null }, { scope: 'playground', agent: null },
{ scope: 'supporting', agent: 'acme/scribe' }, { scope: 'web', agent: null }] };
const changed = ({ path, commit }) => path === 'Notes/Eyes.md' && commit === A;
const cite = (path, lines, quote, commit = A) => ({ path, commit, lines, quote });
const options = {
scopeOf: path => path.includes('/Playground/') ? 'playground' : path.endsWith('.md') ? 'canon' : undefined,
changed: ({ path, commit }) => path === 'Book One/Characters/Mara Vell.md' && commit === A,
};
const render = items => renderItems(JSON.stringify({ items }), options);
test('free text renders as itself with no citations', () => {
for (const body of ['Grey, in chapter 3.', '', '"a JSON string"', '[1, 2]', '42', 'null'])
assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body });
// One snapshot per item kind: the plain text both channels show.
const SNAPSHOTS = {
fact: [{ kind: 'fact', text: 'Tomas is the ferryman.', cites: [cite('Book One/Characters/Tomas.md', [3, 3], 'the ferryman', B)] }],
finding: [{ kind: 'finding', text: 'Eye colour differs.', cites: [cite('Book One/Chapters/Chapter 01.md', [41, 41], 'her grey eyes'),
cite('Book One/Characters/Mara Vell.md', [9, 10], 'green eyes')] }],
correction: [{ kind: 'correction', text: 'Chapter 1 says grey.', from: 'green eyes', to: 'grey eyes',
cites: [cite('Book One/Characters/Mara Vell.md', [9, 9], 'green eyes', B)] }],
question: [{ kind: 'question', text: 'Is the harbor north of the town?', cites: [cite('Book One/Places/Harbor Town.md', [2, 4], 'the harbor', B)] }],
idea: [{ kind: 'idea', text: 'A storm could close the harbor.', cites: [cite('Book One/Playground/What If.md', [5, 5], 'a storm', B),
{ url: 'https://example.invalid/tides', quote: 'spring tides' }] }],
};
const EXPECTED = {
fact: '1. Fact: Tomas is the ferryman.\n [canon] Book One/Characters/Tomas.md:3 @bbbbbbb "the ferryman"',
finding: '1. Finding: Eye colour differs.\n [canon] Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
+ ' [canon] Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes" (changed since)',
correction: '1. Correction: Chapter 1 says grey.\n From: "green eyes"\n To: "grey eyes"\n'
+ ' [canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes"',
question: '1. Question: Is the harbor north of the town?\n [canon] Book One/Places/Harbor Town.md:2-4 @bbbbbbb "the harbor"',
idea: '1. Idea: A storm could close the harbor.\n [playground] Book One/Playground/What If.md:5 @bbbbbbb "a storm"\n'
+ ' [web] https://example.invalid/tides "spring tides"',
};
for (const kind of Object.keys(SNAPSHOTS)) {
test(`renderItems snapshot: ${kind}`, () => {
const out = render(SNAPSHOTS[kind]);
assert.equal(out.kind, 'items');
assert.equal(out.text, EXPECTED[kind]);
assert.equal(out.items[0].kind, kind);
});
}
test('renderItems numbers several items and takes a parsed body as well as a string', () => {
const items = [...SNAPSHOTS.fact, ...SNAPSHOTS.question];
const out = renderItems({ items }, options);
assert.equal(out.text, `${EXPECTED.fact}\n${EXPECTED.question.replace(/^1\./, '2.')}`);
assert.deepEqual(out.items[1].cites[0], { badge: 'canon', path: 'Book One/Places/Harbor Town.md', commit: B, lines: [2, 4],
ref: 'Book One/Places/Harbor Town.md:2-4 @bbbbbbb', quote: 'the harbor', changed: false });
});
test('a cited reply renders its text, then each cite with the badge from the check result', () => {
const out = renderReply(CITED, { check: CHECK, changed });
assert.equal(out.kind, 'reply');
assert.equal(out.text, "Mara's eyes are grey in chapter 3.");
assert.deepEqual(out.citations.map(c => c.badge), ['canon', 'playground', 'supporting', 'web']);
assert.equal(out.plain, "Mara's eyes are grey in chapter 3.\n"
+ ' [canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"\n'
+ ' [playground] Book One/Playground/What If.md:5-6 @bbbbbbb "a storm"\n'
+ ' [supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)\n'
+ ' [web] https://example.invalid/tides "spring tides"');
assert.deepEqual(out.citations[0], { badge: 'canon', path: 'Book One/Chapters/Chapter 03.md', commit: A, lines: [41, 41],
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', quote: 'her grey eyes', canon: true, agent: null, changed: false });
assert.deepEqual(out.citations[3], { badge: 'web', url: 'https://example.invalid/tides', link: 'https://example.invalid/tides',
ref: 'https://example.invalid/tides', quote: 'spring tides', agent: null, changed: false });
test('without options a path cite has no badge and is never marked changed', () => {
assert.equal(renderItems({ items: SNAPSHOTS.finding }).text,
'1. Finding: Eye colour differs.\n Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
+ ' Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes"');
});
test('a parsed body renders as its string does', () => {
assert.deepEqual(renderReply(JSON.parse(CITED), { check: CHECK, changed }), renderReply(CITED, { check: CHECK, changed }));
});
const NOTICE_SNAPSHOTS = { 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' };
for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
test(`renderItems snapshot: notice ${notice}`, () => {
assert.deepEqual(renderItems(JSON.stringify({ items: [], notice }), options), { kind: 'notice', notice, text });
});
}
test('without a check result a path cite has no badge, no label and is never changed', () => {
const out = renderReply(CITED);
assert.deepEqual(out.citations.map(c => [c.badge, c.agent, c.changed]),
[[null, null, false], [null, null, false], [null, null, false], ['web', null, false]]);
});
test('the label and the badge come from the check result only, never from a bad entry', () => {
const bad = { citations: [{ scope: 'web', agent: '' }, { scope: 'story', agent: 7 }, null, { scope: 'canon', agent: 'x/y' }] };
const out = renderReply(CITED, { check: bad });
// A path cite can't be web; a url cite is web by its shape whatever the check says, and never agent-written.
assert.deepEqual(out.citations.map(c => [c.badge, c.agent]), [[null, null], [null, null], [null, null], ['web', null]]);
});
test('a web cite is a link only for https', () => {
const urls = { 'https://example.invalid/a': 'https://example.invalid/a', 'http://example.invalid/a': null,
'javascript:alert(1)': null, 'JAVASCRIPT:alert(1)': null, 'data:text/html,x': null, 'not a url': null, '//example.invalid': null };
for (const [url, link] of Object.entries(urls)) {
const out = renderReply(reply('t', [web(url)]));
assert.equal(out.kind, 'reply', url);
assert.equal(out.citations[0].link, link, url);
assert.equal(out.citations[0].ref, url);
test('a body without the reply shape shows a fixed line and none of its text', () => {
const secret = 'free prose the guard would refuse';
const bodies = [secret, JSON.stringify(secret), '{"items": [], "notice": "other"}', JSON.stringify({ items: [], notice: 'no-answer', extra: secret }),
JSON.stringify({ items: [] }), JSON.stringify({ items: [{ kind: 'story', text: secret, cites: [] }] }),
JSON.stringify({ items: [{ kind: 'fact', text: secret }] }), JSON.stringify({ items: SNAPSHOTS.fact, prose: secret }),
JSON.stringify({ items: [{ kind: 'correction', text: secret, cites: [] }] }),
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [cite('a.md', [2, 1], 'q')] }] }),
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }] }] }),
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }] }] }),
null, 42, '[]'];
for (const body of bodies) {
const out = renderItems(body, options);
assert.deepEqual(out, { kind: 'unreadable', text: UNREADABLE }, String(body));
assert.ok(!JSON.stringify(out).includes(secret));
}
});
test('a write notice renders its fixed line', () => {
const body = op => JSON.stringify({ notice: 'library-write', op, path: 'Notes/Eyes.md', commit: B });
assert.deepEqual(renderReply(body('write'), { name: 'Scribe' }),
{ kind: 'notice', op: 'write', path: 'Notes/Eyes.md', commit: B, text: 'Scribe wrote Notes/Eyes.md @bbbbbbb', plain: 'Scribe wrote Notes/Eyes.md @bbbbbbb' });
assert.equal(renderReply(body('delete'), { name: 'Scribe' }).text, 'Scribe deleted Notes/Eyes.md @bbbbbbb');
assert.equal(renderReply(body('write')).text, 'An agent wrote Notes/Eyes.md @bbbbbbb');
});
test('a body without the reply or notice shape is free text: the whole body, no citations', () => {
const bodies = [
JSON.stringify({ text: 't' }), JSON.stringify({ citations: [] }), JSON.stringify({ text: 1, citations: [] }),
JSON.stringify({ text: 't', citations: [], extra: 1 }), JSON.stringify({ items: [] }),
reply('t', [cite('a.md', [2, 1], 'q')]), reply('t', [cite('a.md', [0, 1], 'q')]), reply('t', [cite('a.md', [1], 'q')]),
reply('t', [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }]), reply('t', [cite('a.md', [1, 1], 'q', A, { as: 'playground' })]),
reply('t', [cite('a.md', [1, 1], 'q', A, { scope: 'canon' })]), reply('t', [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }]),
reply('t', [{ url: 'https://x.invalid' }]), reply('t', [{ url: 'https://x.invalid', quote: 'q', agent: 'x/y' }]),
JSON.stringify({ notice: 'library-write', op: 'move', path: 'a.md', commit: A }),
JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: 'abc' }),
JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: A, by: 'x' }),
];
for (const body of bodies) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }, body);
});
test('an empty citations list is a reply with no cites', () => {
assert.deepEqual(renderReply(reply('Grey.', [])), { kind: 'reply', text: 'Grey.', citations: [], plain: 'Grey.' });
});
-2
View File
@@ -189,7 +189,5 @@ const STATUS = {
// leaves the remote unreachable, like no bus to ask.
'library-sync-busy': 409, 'library-diverged': 409, 'library-accept-stale': 409, 'library-accept-invalid': 400,
'library-credential-missing': 503, 'library-fetch-failed': 503,
// Row 225 (C2-10b): a Revert of a commit that isn't an agent's is hers to read, like stale-base.
'library-revert-not-agent': 409,
};
export const busStatus = code => STATUS[code] ?? 502;
+3 -3
View File
@@ -68,14 +68,14 @@ try {
// the system config, Business can't resolve authority and Settings has
// no notifier. A refusal printed here names no path (redactor).
// --send without a bus refuses to start rather than serve a composer that can't send.
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null;
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null;
let processing = null, ownerCall = null;
try {
system = loadSystem();
const { dataRoot } = system;
const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
// FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs).
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn }));
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn }));
// Row 252 (#1753): Approve and Deny on grant requests, through the console wrapper, only with
// --sso and --send.
if (ssoOn && sendOn) decisionWrite = consoleCall({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
@@ -107,7 +107,7 @@ try {
}
let server;
try {
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null });
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null });
} catch (err) { sso?.close(); throw err; }
let testerServer = null;
if (testerSocket) {
+2 -2
View File
@@ -30,10 +30,10 @@ export function consoleBus(call, { send: sendOn = false, owner = null } = {}) {
projectFiles: args => call('project.files', args),
projectFile: args => call('project.file', args),
libraryFile: args => call('library.file', args), libraryStatus: args => call('library.status', args),
send: null, librarySync: null, libraryRevert: null, settingsWrite: null, projectSave: null, nameWrite: null, assistantStop: null, instancesWrite: null, routesWrite: null, agentWrite: null,
send: null, librarySync: null, settingsWrite: null, projectSave: null, nameWrite: null, assistantStop: null, instancesWrite: null, routesWrite: null, agentWrite: null,
};
if (sendOn) {
o.send = args => call('message.send', args); o.librarySync = args => call('library.sync', args); o.libraryRevert = args => call('library.revert', args); o.settingsWrite = args => call('limits.set', args);
o.send = args => call('message.send', args); o.librarySync = args => call('library.sync', args); o.settingsWrite = args => call('limits.set', args);
o.projectSave = args => call('project.file.save', args); o.nameWrite = args => call('name.set', args); o.assistantStop = args => call('assistant.stop', args);
o.instancesWrite = args => call('instances.set', args); o.routesWrite = args => call('routes.set', args); o.agentWrite = agentCaller(call, AGENT_VERBS);
}
-17
View File
@@ -23,20 +23,3 @@
.chat-session-controls label { display: block; }
.chat-session-controls select, .chat-session-controls input { max-width: 100%; }
.chat-session-controls form { margin: .6rem 0; }
/* Row 225 (C2-10b, design 7.4): a reply's cites, a write notice and the library status line. The
badge and labels are text, so they read the same without colour. */
.chat-cites { list-style: none; margin: .5rem 0 0; padding-inline-start: 1rem; display: grid; gap: .35rem; }
.chat-log .chat-cites li { padding: 0; border-bottom: 0; overflow-wrap: anywhere; font-size: .92rem; }
.chat-cites code { font-family: var(--mono); font-size: .85rem; }
.chat-scope, .chat-label { display: inline-block; padding: 0 .4rem; border: 1px solid var(--line); border-radius: var(--r); font-size: .78rem; }
.chat-scope-canon { border-color: var(--success); color: var(--success); }
.chat-scope-playground { border-color: var(--accent); color: var(--accent); }
.chat-scope-web { border-color: var(--action); color: var(--action); }
.chat-agent-written { color: var(--muted); }
.chat-changed { border-color: var(--warning); color: var(--warning); }
.chat-notice { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; white-space: normal; }
.chat-revert-receipt { overflow-wrap: anywhere; }
.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; }
+4 -103
View File
@@ -17,22 +17,11 @@
if (!forms.has(key)) forms.set(key, { name: '', repo: '', instance: '', instanceChosen: false, receipt: '', pending: false });
return forms.get(key);
};
// Row 225 (C2-10b, design 7.4): a reply renders through the library's renderReply, the one renderer the
// Discord relay shares. Until it loads, or if it can't, a body shows as its text. Badges, the
// agent-written label and "changed since" come from the bus's stored check (C2-06), never the body.
const SHA = /^[0-9a-f]{40}$/, CODE = /^[a-z][a-z-]{0,63}$/, NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/;
// 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; };
let renderReply = null;
import('/render.mjs').then(m => { renderReply = m.renderReply; if (current && !out()) paint(); }, () => {});
const libraries = new Map(), reverts = new Map();
class ChatError extends Error {
constructor(code, status = 0, sent) { super(code); this.code = code; this.status = status; this.sent = sent; }
}
function forget() {
generation++; controller?.abort(); states.clear(); forms.clear(); libraries.clear(); reverts.clear();
generation++; controller?.abort(); states.clear(); forms.clear();
if (current) current.container.replaceChildren();
current = null;
}
@@ -91,93 +80,11 @@
const reason = !Number.isSafeInteger(selected?.revision) ? 'Safe clear is unavailable until its dependency is installed.' : '';
return `<section class="chat-session-controls" aria-label="Sessions"><label>Session<select id="chat-session">${c.sessions.map(r => `<option value="${esc(r.id)}"${r.id === c.session ? ' selected' : ''}>${txt(r.name)}${r.default ? ' (default)' : ''} — ${txt(r.state ?? 'status unavailable')}</option>`).join('')}</select></label><p id="chat-compactions">Compactions: ${txt(countLabel(selected?.compactions))}</p><p>${txt(selected?.state ?? 'Session status unavailable')}${UUID.test(selected?.successor) ? ` · <a href="${sessionPath(t.id, selected.successor)}">Open fresh successor</a>` : ''}</p><form id="chat-session-form" class="chat-create" data-key="session:${esc(t.id)}"><label>New session name<input id="chat-session-name" data-chat-field="name" value="${esc(f.name)}" maxlength="200" required${disabled(f.pending)}></label><button class="btn" type="submit"${disabled(f.pending)}>New session</button><p role="status">${txt(f.receipt)}</p></form><button class="btn" type="button" id="chat-clear"${disabled(clear.pending || !!reason || selected?.state && selected.state !== 'active')}>Clear context</button>${reason ? `<p>${txt(reason)}</p>` : ''}<p id="chat-clear-receipt" role="status">${txt(clear.receipt)}</p><form id="chat-operation-form"><label>Check clear operation<input id="chat-operation" value="${esc(clear.op ?? '')}" maxlength="160"${clear.pending ? ' disabled' : ''}></label><button class="btn" type="submit">Check status</button><button class="btn" type="button" id="chat-cancel"${disabled(clear.pending || !!reason || !clear.op || ['cancelled', 'failed'].includes(clear.record?.state) || clear.record?.start === 'started')}>Cancel clear</button></form></section>`;
}
// A cite is changed when the file's last commit at the served tip (`last`, read with the page) is not
// its last commit at the cited one (`file`, stored with the check). Either one missing marks nothing.
const changedBy = check => (cite, i) => {
const e = Array.isArray(check?.citations) ? check.citations[i] : null;
return SHA.test(e?.file ?? '') && SHA.test(e?.last ?? '') && e.file !== e.last;
};
function cites(list) {
return `<ul class="chat-cites">${list.map(c => `<li>${c.badge ? `<span class="chat-scope chat-scope-${c.badge}">${txt(c.badge)}</span> ` : ''}${c.link ? `<a href="${esc(c.link)}" rel="noopener noreferrer" target="_blank">${txt(c.ref)}</a>` : `<code>${txt(c.ref)}</code>`} <q>${txt(c.quote)}</q>${c.agent ? ` <span class="chat-label chat-agent-written">agent-written: ${txt(c.agent)}</span>` : ''}${c.changed ? ' <span class="chat-label chat-changed">changed since</span>' : ''}</li>`).join('')}</ul>`;
}
function messageBody(m) {
const r = renderReply?.(m.body, { check: m.check ?? null, name: m.from_role, changed: changedBy(m.check) });
if (r?.kind === 'reply') return `<div class="chat-body">${txt(r.text)}</div>${r.citations.length ? cites(r.citations) : ''}`;
if (r?.kind === 'notice') {
const v = reverts.get(r.commit) ?? {};
return `<div class="chat-body chat-notice"><span>${txt(r.text)}</span> <button class="btn" type="button" data-revert="${esc(r.commit)}"${disabled(v.pending)}>Revert</button> <span class="chat-revert-receipt" role="status">${txt(v.receipt ?? '')}</span></div>`;
}
return `<div class="chat-body">${txt(m.body)}</div>`;
}
const ago = iso => {
const minutes = Math.floor((Date.now() - Date.parse(iso)) / 60000);
return !Number.isFinite(minutes) ? 'at an unknown time' : minutes < 1 ? 'just now' : minutes < 60 ? `${minutes} min ago` : minutes < 2880 ? `${Math.floor(minutes / 60)} h ago` : `${Math.floor(minutes / 1440)} days ago`;
};
// library.status (C2-04): the served commit and when it was last fetched, a held divergence, the last
// fetch error, the reply limits in force (C2-06) and the R-problems, listed one click away.
function libraryLine(project) {
const l = libraries.get(project);
if (!l || l.hidden) return '';
if (l.error) return `<div class="chat-library"><p role="status">Library status unavailable (${txt(l.error)}).</p></div>`;
const v = l.value, parts = [];
parts.push(SHA.test(v.served ?? '') ? `Library at <code>${txt(v.served.slice(0, 7))}</code>` : 'Library not served yet');
if (typeof v.lastFetch === 'string') parts.push(`fetched ${txt(ago(v.lastFetch))}`);
if (SHA.test(v.diverged?.commit ?? '')) parts.push(`<b>library-diverged</b> at <code>${txt(v.diverged.commit.slice(0, 7))}</code>`);
if (CODE.test(v.lastError?.code ?? '')) parts.push(`fetch error <code>${txt(v.lastError.code)}</code>`);
const limits = v.replyLimits;
if (limits && typeof limits === 'object') {
parts.push(`citations ${limits.requireCitation === true ? 'required' : 'optional'}${Number.isSafeInteger(limits.citationsPerReply) ? `, up to ${limits.citationsPerReply} per reply` : ''}`);
const fell = Array.isArray(limits.fallback) ? limits.fallback.filter(k => typeof k === 'string') : [];
if (fell.length) parts.push(`limits fell back: ${fell.map(k => `<code>${txt(k)}</code>`).join(', ')}`);
if (CODE.test(limits.problem ?? '')) parts.push(`<code>${txt(limits.problem)}</code>`);
}
const problems = Array.isArray(v.problems) ? v.problems : [];
if (!problems.length) parts.push('no problems');
const list = problems.length ? `<details class="chat-library-problems"><summary>${problems.length} problem${problems.length === 1 ? '' : 's'}</summary><ul>${problems.map(p => `<li><code>${txt(p?.code)}</code> ${txt(p?.path)}${Number.isSafeInteger(p?.line) ? ':' + p.line : ''} ${txt(p?.message)}</li>`).join('')}</ul></details>` : '';
return `<div class="chat-library"><p role="status">${parts.join(' · ')}</p>${list}</div>`;
}
async function loadLibrary(project, g, signal) {
if (project === null) return;
let entry;
try {
const res = await fetch('/api/library/status?project=' + encodeURIComponent(project), { cache: 'no-store', redirect: 'manual', signal });
if (res.type === 'opaqueredirect' || res.status === 401) { signOut(); return; }
const data = await res.json().catch(() => null);
entry = res.ok && data && typeof data === 'object' && !Array.isArray(data) ? { value: data } : { error: CODE.test(data?.error ?? '') ? data.error : 'invalid-response' };
} catch (err) {
if (err.name === 'AbortError') return;
entry = { error: 'unreachable' };
}
if (g !== generation || out()) return;
libraries.set(project, NO_LIBRARY.includes(entry.error) ? { hidden: true } : entry);
paint();
}
// design (writes): Revert is library.revert {commit}, her act. A refusal shows its code; before
// C2-02's verb exists that is the bus's refusal of the verb.
async function revert(commit) {
const c = current, t = c?.threads.find(t => t.id === c.id), v = reverts.get(commit) ?? {};
const project = libraryOf(c, t);
if (!c || !c.canSend || out() || v.pending || !SHA.test(commit) || project === null) return;
if (!window.confirm(`Revert ${commit.slice(0, 7)}? This makes a new commit in the library that puts the file back as it was before it.`)) return;
const unknown = 'Outcome unknown. Read the library status before trying again; nothing was retried.';
reverts.set(commit, { pending: true, receipt: 'Reverting…' }); paint();
let receipt;
try {
const res = await fetch('/api/library/revert', { method: 'POST', cache: 'no-store', redirect: 'manual', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ project, commit }) });
if (res.type === 'opaqueredirect' || res.status === 401) { signOut(); return; }
const data = await res.json().catch(() => null);
receipt = res.ok && SHA.test(data?.commit ?? '') ? `Reverted in ${data.commit.slice(0, 7)}.`
: data?.sent === false && typeof data.error === 'string' ? `Not reverted (${inert(data.error.slice(0, 80))}).` : unknown;
} catch { receipt = unknown; }
if (out()) return;
reverts.set(commit, { pending: false, receipt });
if (current) { paint(); void loadLibrary(project, generation); }
}
function conversation(t) {
const s = state(current.key);
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div>${messageBody(m)}</li>`).join('');
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div><div class="chat-body">${txt(m.body)}</div></li>`).join('');
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)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><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><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>`;
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)}${sessionControls(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><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><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>`;
}
function capture() {
if (!current) return null;
@@ -326,11 +233,7 @@
}
} catch (err) { if (err.name === 'AbortError') return; }
}
if (g === generation) {
paint(navigated);
const t = kind === 'thread' ? current.threads.find(t => t.id === id) : null;
if (t) void loadLibrary(libraryOf(current, t), g, signal);
}
if (g === generation) paint(navigated);
} catch (err) {
if (g !== generation || out() || err.name === 'AbortError' || err.code === 'signed-out') return;
current.error = readError(err);
@@ -437,8 +340,6 @@
if (event.target.id === 'chat-cancel') void cancelClear();
if (event.target.id === 'chat-clear') void clearContext();
if (event.target.id === 'chat-next') void refresh(true);
const revertButton = event.target.closest?.('[data-revert]');
if (revertButton) void revert(revertButton.dataset.revert);
if (event.target.id === 'chat-new') { state(current.key ?? current.id).tailRequested = true; state(current.key ?? current.id).follow = true; paint(); }
});
document.addEventListener('change', event => {
+5 -60
View File
@@ -32,8 +32,6 @@ const files = new Map([
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']],
// Row 225 (C2-10b): the one reply renderer, shared with the Discord relay, imported by chat.js.
['/render.mjs', ['../../../library/src/render.mjs', 'text/javascript; charset=utf-8']],
['/editor.css', ['editor.css', 'text/css; charset=utf-8']],
['/icons.svg', ['icons.svg', 'image/svg+xml']],
...[400, 500, 600, 700].map(w => [`/assets/fonts/manrope-${w}.woff2`, [`assets/fonts/manrope-${w}.woff2`, 'font/woff2']]),
@@ -44,7 +42,6 @@ const files = new Map([
const USER_FILES = new Map([
['/', ['user.html', 'text/html; charset=utf-8']],
...['user.js', 'chat.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
['/render.mjs', files.get('/render.mjs')],
...['tokens.css', 'console.css', 'chat.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
...[...files].filter(([p]) => p.startsWith('/assets/fonts/') || p === '/icons.svg'),
]);
@@ -599,55 +596,12 @@ async function projectFileRead(res, read, path, search) {
return json(res, busStatus(code), { error: code, message: err instanceof BusReadError ? err.message : code });
}
}
// Row 225 (C2-10b, design 7.4): the chat's library status line and a write notice's Revert. The status
// is a read like the file list. The revert is her write (C2-02's `library.revert`), with the save's
// boundary: header user, exact Origin, same-origin Fetch Metadata, JSON, strict keys. Until the verb
// exists the bus refuses it, and the page shows that code.
const REVERT_KEYS = ['project', 'commit'];
async function libraryRevertSend(req, res, { libraryRevert, preview, log }) {
let target = null;
const line = outcome => log(`${new Date().toISOString()} library-revert user=${JSON.stringify(preview.user)} project=${quoted(target?.project)} commit=${quoted(target?.commit)} ${outcome}`);
const refuse = (status, error, message, sent = false) => { line(`error=${error}`); return json(res, status, { error, message, sent }); };
if (!signedIn(req, preview)) return json(res, 401, { error: 'sign-in required', message: 'sign in again', sent: false });
if (one(req, 'origin') !== ownOrigin(preview)) return refuse(403, 'cross-origin request refused', 'a revert needs the Console\'s own Origin');
if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site request refused', 'a revert needs same-origin Fetch Metadata');
let value;
try { value = JSON.parse((await body(req)).toString('utf8')); } catch (err) { return refuse(400, 'invalid-request', err.message); }
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== REVERT_KEYS.length ||!REVERT_KEYS.every(k => typeof value[k] === 'string') || !PATH_SHAPE.test(value.project) || !COMMIT.test(value.commit)) {
return refuse(400, 'invalid-request', 'a revert takes a project and a 40-hex commit, nothing else');
}
target = value;
let result;
try { result = await libraryRevert({ project: value.project, commit: value.commit }); } catch (err) {
const { code, sent } = sendOutcome(err);
return sent === false
? refuse(busStatus(code), code, `not reverted: the bus refused it (${code}).`)
: refuse(503, 'outcome-unknown', 'the revert may have been made; nothing was retried. Read the library status before trying again', 'unknown');
}
if (!COMMIT.test(result?.commit) || result.project !== value.project || result.reverted !== value.commit || typeof result.path !== 'string') return refuse(503, 'outcome-unknown', 'the bus answered without the new commit; the revert may have been made', 'unknown');
line(`commit=${result.commit}`);
return json(res, 200, { project: value.project, path: result.path, reverted: result.reverted, commit: result.commit, at: new Date().toISOString() });
}
async function libraryStatusRead(res, read, search) {
const query = new URLSearchParams(search);
if ([...query.keys()].some(k => k !== 'project') || query.getAll('project').length !== 1 || !PATH_SHAPE.test(query.get('project')))
return json(res, 400, { error: 'invalid-request', message: 'a library status read takes a project' });
if (!read) return json(res, 503, { error: 'not-configured', message: 'the Console has no bus configured' });
try {
const result = await read({ project: query.get('project') });
if (result === null || typeof result !== 'object' || Array.isArray(result) || result.project !== query.get('project')) throw new BusReadError('invalid-response');
return json(res, 200, { ...result, at: new Date().toISOString() });
} catch (err) {
const code = err instanceof BusReadError ? err.code : 'invalid-response';
return json(res, busStatus(code), { error: code, message: err instanceof BusReadError ? err.message : code });
}
}
// `send` is the bus write (row 76): `args => Promise<{id}>`, only with the preview mode or `sso`.
// `sso` is createSso() from sso.mjs (Q175 phase 1): the human listener signs people in itself.
// `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for
// tests; the field table is otherwise the committed packages/tasks/fields.json.
// `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request.
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host');
if (host === 'localhost') host = '127.0.0.1';
if (tester && preview !== null) throw new Error('tester listener cannot use preview mode');
@@ -655,7 +609,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (sso !== null && (preview !== null || proof !== null)) throw new Error('--sso cannot be combined with --auth-header, --allow-user or --proof-*');
if (tester && host !== '127.0.0.1') throw new Error('tester listener must bind 127.0.0.1');
// Row 177 (#1675): chat is allowed. The tester listener's chat binds the tester, so the bus keeps it on tester threads.
if (tester && [send, settingsWrite, projectSave, libraryRevert, assistantStop].some(value => value !== null)) throw new Error('tester listener permits only instance, route, name and chat writes');
if (tester && [send, settingsWrite, projectSave, assistantStop].some(value => value !== null)) throw new Error('tester listener permits only instance, route, name and chat writes');
if (tester && !testerSocket) throw new Error('tester listener requires private socket');
if (!tester && testerSocket) throw new Error('tester socket requires tester listener');
// Row 252 (#1753, design section 4): Console decision answers need --sso and --send, never the tester.
@@ -665,7 +619,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
// `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session
// user's own transport (console-bus.mjs). Only a signed-in listener has user sessions.
if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso');
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files };
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files };
const hub = federationView(federation);
// An object with none of the three is refused too: asking for the preview never serves without it.
if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) {
@@ -675,7 +629,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
const server = createServer(async (req, res) => {
// A granted user's request rebinds these below; every other request keeps the owner's.
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
res.setHeader('x-content-type-options', 'nosniff');
res.setHeader('referrer-policy', 'no-referrer');
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
@@ -742,7 +696,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true;
reads = null; upstream = null; files = USER_FILES; granted = true;
}
}
if (named && preview !== null) {
@@ -794,11 +748,6 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if ((!tester && (!send || !named)) || !routesWrite) return json(res, 403, { error: 'read-only', sent: false });
return routesSend(req, res, { routesWrite, preview: writer, log });
}
if (path === '/api/library/revert') {
if (req.method !== 'POST') return json(res, 405, { error: 'method not allowed' });
if (!send || !libraryRevert || !named) return json(res, 403, { error: 'read-only', sent: false });
return libraryRevertSend(req, res, { libraryRevert, preview: signer, log });
}
if (path === '/api/project-file' && req.method === 'POST') {
if (!send || !projectSave || !named) return json(res, 403, { error: 'read-only', sent: false });
return projectFileSend(req, res, { projectSave, preview: signer, log });
@@ -867,10 +816,6 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
return consoleRead(res, () => fieldTable(fieldsFile));
}
if (path === '/api/library/status') {
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
return libraryStatusRead(res, libraryStatus, search);
}
if (path === '/api/project-files' || path === '/api/project-file') {
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
return projectFileRead(res, path === '/api/project-file' ? projectFile : projectFiles, path, search);
@@ -1,205 +0,0 @@
// Row 225 (C2-10b, design 7.4): replies, cites, write notices and the library status line in the
// Console chat. Fixture only: the chat and library answers are stubbed; /render.mjs is the real one.
import test from 'node:test';
import assert from 'node:assert/strict';
import { startServer } from '../src/serve.mjs';
import { fixture, close } from './fixture.mjs';
import { browser } from './browser.mjs';
const P = '10000000-0000-4000-8000-000000000001';
const A = '20000000-0000-4000-8000-000000000002';
const C1 = 'a'.repeat(40), C2 = 'b'.repeat(40), C3 = 'c'.repeat(40), C4 = 'd'.repeat(40);
const HOSTILE = '<img src=x onerror="window.injected=1">';
const cite = (path, lines, quote, commit = C1, extra = {}) => ({ path, commit, lines, quote, ...extra });
const reply = (text, citations) => JSON.stringify({ text, citations });
const row = (id, body, check, from = 'assistant') => ({ id: `5000000${id}-0000-4000-8000-000000000000`, from_role: from, at: '2026-10-23T18:00:00.000Z', body, ...(check === undefined ? {} : { check }) });
const ROWS = [
row(1, 'Plain text answer.', undefined, 'jason'),
row(2, reply("Mara's eyes are grey.", [
cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', C1, { as: 'canon' }),
cite('Book One/Playground/What If.md', [5, 6], 'a storm'),
cite('Notes/Eyes.md', [9, 10], 'green eyes'),
{ url: 'https://example.invalid/tides', quote: 'spring tides' },
{ url: 'http://example.invalid/plain', quote: 'plain http' },
{ url: 'javascript:window.injected=2', quote: 'script' },
]), { citations: [
{ scope: 'canon', agent: null, file: C1, last: C1 },
{ scope: 'playground', agent: null, file: C2, last: C3 },
{ scope: 'supporting', agent: 'acme/scribe', file: C2 },
{ scope: 'web', agent: null }, { scope: 'web', agent: null }, { scope: 'web', agent: null },
] }),
row(3, reply('Unchecked.', [cite('Notes/Eyes.md', [1, 1], HOSTILE)])),
row(4, JSON.stringify({ notice: 'library-write', op: 'write', path: 'Notes/Eyes.md', commit: C4 }), undefined, 'scribe'),
];
const STATUS = { project: 'books', version: 1, branch: 'main', served: C3, lastFetch: 'FIVE-MINUTES-AGO',
lastError: { code: 'library-fetch-failed', at: '2026-10-23T18:00:00.000Z' }, diverged: { commit: C2, at: '2026-10-23T18:00:00.000Z' },
problems: [{ code: 'R1', path: 'Notes/Eyes.md', message: 'no heading', line: 3 }, { code: 'R2', path: HOSTILE, message: 'bad name' }] };
const stub = `(() => {
const real = fetch.bind(window);
window.__reverts=[];window.__revertAnswers=[];window.__statusReads=[];window.__confirms=[];
window.__status={status:200,body:${JSON.stringify(STATUS)}};
window.confirm=text=>{window.__confirms.push(text);return window.__confirm!==false;};
window.fetch=async (url,init={})=>{
const path=String(url), method=init.method||'GET';
const answer=(value,status=200)=>new Response(JSON.stringify(value),{status,headers:{'content-type':'application/json'}});
if(path==='/api/config')return answer({send:!location.search.includes('readonly')});
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')){
window.__statusReads.push(path);
const body={...window.__status.body};
if(body.lastFetch==='FIVE-MINUTES-AGO')body.lastFetch=new Date(Date.now()-330000).toISOString();
return answer(body,window.__status.status);
}
if(path==='/api/library/revert'){
window.__reverts.push({method,body:JSON.parse(init.body),headers:init.headers,redirect:init.redirect});
const a=window.__revertAnswers.shift();if(!a)throw new Error('No revert answer');
if(a.reject)throw new TypeError('network');
return answer(a.body,a.status||200);
}
if(!path.startsWith('/api/chat'))return real(url,init);
const data=value=>answer({...value,at:new Date().toISOString()});
if(path==='/api/chat/instances')return data({rows:[{instance:'assistant',name:'assistant'}]});
if(path==='/api/chat/projects')return data({rows:[{id:'${P}',name:'Books',repo:window.__repo===undefined?'books':window.__repo}]});
if(path.endsWith('/sessions'))return data({rows:[{id:'${A}',name:'default',default:true}]});
if(path.startsWith('/api/chat/threads')&&!path.includes('/messages'))return data({rows:[{id:'${A}',project:'${P}',name:'alpha',instance:'assistant'}]});
return data({rows:${JSON.stringify(ROWS)},after:null,hasMore:false});
};
})();`;
async function setup(t, { suffix = '', before = '' } = {}) {
const f = await fixture(); t.after(() => f.close());
const server = await startServer({ port: 0, board: f.boardURL }); t.after(() => close(server));
const b = await browser(); t.after(() => b.close());
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub + before });
await b.navigate(`http://127.0.0.1:${server.address().port}/${suffix}#/chat/threads/${A}`);
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return;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 message = n => `.chat-log > li:nth-child(${n})`;
await wait('document.querySelector(".chat-cites")');
return { b, wait, text, message };
}
test('chat replies: free text, cites with badges, the agent label, changed since and safe links', async t => {
const { b, text, message } = await setup(t);
assert.equal(await text(`${message(1)} .chat-body`), 'Plain text answer.');
assert.equal(await text(`${message(2)} .chat-body`), "Mara's eyes are grey.");
const cites = await b.evaluate(`[...document.querySelectorAll(${JSON.stringify(message(2) + ' .chat-cites > li')})].map(li => ({
badge: li.querySelector('.chat-scope')?.textContent ?? null, ref: (li.querySelector('a') ?? li.querySelector('code'))?.textContent,
href: li.querySelector('a')?.getAttribute('href') ?? null, rel: li.querySelector('a')?.getAttribute('rel') ?? null,
quote: li.querySelector('q').textContent, agent: li.querySelector('.chat-agent-written')?.textContent ?? null,
changed: !!li.querySelector('.chat-changed') }))`);
assert.deepEqual(cites, [
{ badge: 'canon', ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', href: null, rel: null, quote: 'her grey eyes', agent: null, changed: false },
{ badge: 'playground', ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa', href: null, rel: null, quote: 'a storm', agent: null, changed: true },
// `last` absent: nothing is marked changed (sage-3596).
{ badge: 'supporting', ref: 'Notes/Eyes.md:9-10 @aaaaaaa', href: null, rel: null, quote: 'green eyes', agent: 'agent-written: acme/scribe', changed: false },
{ badge: 'web', ref: 'https://example.invalid/tides', href: 'https://example.invalid/tides', rel: 'noopener noreferrer', quote: 'spring tides', agent: null, changed: false },
{ badge: 'web', ref: 'http://example.invalid/plain', href: null, rel: null, quote: 'plain http', agent: null, changed: false },
{ badge: 'web', ref: 'javascript:window.injected=2', href: null, rel: null, quote: 'script', agent: null, changed: false },
]);
// A reply with no check result: no badge, no label, never changed; its quote is inert text.
assert.equal(await b.evaluate(`document.querySelectorAll(${JSON.stringify(message(3) + ' .chat-scope, ' + message(3) + ' .chat-label')}).length`), 0);
assert.equal(await text(`${message(3)} q`), HOSTILE);
assert.equal(await b.evaluate('!!(document.querySelector(".chat-log img") || window.injected)'), false);
});
test('chat replies: a write notice reads as its line with a Revert; a refused revert shows its code', async t => {
const { b, wait, text, message } = await setup(t);
assert.equal(await text(`${message(4)} .chat-notice > span`), 'scribe wrote Notes/Eyes.md @ddddddd');
assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(message(4) + ' [data-revert]')}).dataset.revert`), C4);
assert.equal(await b.evaluate('document.querySelectorAll("[data-revert]").length'), 1, 'only the notice has a Revert');
// Cancelled at the confirm: nothing is sent.
await b.evaluate('window.__confirm=false;document.querySelector("[data-revert]").click();true');
assert.equal(await b.evaluate('window.__reverts.length'), 0);
assert.match(await b.evaluate('window.__confirms[0]'), /^Revert ddddddd\?/);
// Before C2-02 the bus refuses the verb; the page shows that code and doesn't retry.
await b.evaluate(`window.__confirm=true;window.__revertAnswers.push({status:502,body:{error:'unknown-verb',message:'not reverted: the bus refused it (unknown-verb).',sent:false}});document.querySelector("[data-revert]").click();true`);
await wait('document.querySelector(".chat-revert-receipt").textContent.startsWith("Not")');
assert.equal(await text('.chat-revert-receipt'), 'Not reverted (unknown-verb).');
assert.deepEqual(await b.evaluate('window.__reverts'), [{ method: 'POST', body: { project: 'books', commit: C4 }, headers: { 'content-type': 'application/json' }, redirect: 'manual' }]);
for (const [answer, receipt] of [
[{ status: 409, body: { error: 'library-revert-not-agent', sent: false } }, 'Not reverted (library-revert-not-agent).'],
[{ status: 409, body: { error: 'stale-base', sent: false } }, 'Not reverted (stale-base).'],
[{ status: 503, body: { error: 'outcome-unknown', sent: 'unknown' } }, 'Outcome unknown. Read the library status before trying again; nothing was retried.'],
[{ reject: true }, 'Outcome unknown. Read the library status before trying again; nothing was retried.'],
[{ body: { project: 'books', path: 'Notes/Eyes.md', reverted: C4, commit: C1 } }, 'Reverted in aaaaaaa.'],
]) {
const n = await b.evaluate('window.__reverts.length');
await b.evaluate(`window.__revertAnswers.push(${JSON.stringify(answer)});document.querySelector("[data-revert]").click();true`);
await wait(`window.__reverts.length===${n + 1} && !document.querySelector("[data-revert]").disabled`);
assert.equal(await text('.chat-revert-receipt'), receipt);
}
assert.ok(await b.evaluate('window.__statusReads.length') >= 2, 'the status line is read again after a revert');
});
test('chat replies: a read-only Console shows the notice with Revert disabled', async t => {
const { b, message } = await setup(t, { suffix: '?readonly' });
assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(message(4) + ' [data-revert]')}).disabled`), true);
await b.evaluate('document.querySelector("[data-revert]").click();true');
assert.equal(await b.evaluate('window.__reverts.length + window.__confirms.length'), 0);
});
test('chat status line: served commit and age, divergence, fetch error and the problems one click away', async t => {
const { b, wait, text } = await setup(t);
await wait('document.querySelector(".chat-library")');
assert.equal(await b.evaluate('window.__statusReads[0]'), '/api/library/status?project=books');
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · library-diverged at bbbbbbb · fetch error library-fetch-failed');
assert.equal(await text('.chat-library-problems summary'), '2 problems');
assert.deepEqual(await b.evaluate('[...document.querySelectorAll(".chat-library-problems li")].map(li => li.textContent)'),
['R1 Notes/Eyes.md:3 no heading', `R2 ${HOSTILE} bad name`]);
assert.equal(await b.evaluate('!!document.querySelector(".chat-library img")'), false);
});
test('chat status line: limits when the status carries them, no problems, and refusals', async t => {
const limits = { ...STATUS, lastError: null, diverged: null, problems: [], replyLimits: { requireCitation: true, citationsPerReply: 4, fallback: ['citationsPerReply'], problem: null } };
const { b, wait, text } = await setup(t, { before: `window.__status={status:200,body:${JSON.stringify(limits)}};` });
await wait('document.querySelector(".chat-library")');
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems');
assert.equal(await b.evaluate('!!document.querySelector(".chat-library-problems")'), false);
for (const [status, error, shown] of [[502, 'library-state-invalid', 'Library status unavailable (library-state-invalid).'], [502, 'Not A Code', 'Library status unavailable (invalid-response).'],
[403, 'owner-only', null], [502, 'project-not-declared', null], [502, 'library-mapping-invalid', 'Library status unavailable (library-mapping-invalid).'], [503, 'project-not-configured', null], [503, 'not-configured', null]]) {
await wait('!document.querySelector("#refresh").disabled');
await b.evaluate(`window.__status={status:${status},body:{error:${JSON.stringify(error)}}};document.querySelector("#refresh").click();true`);
await wait(`window.__statusReads.length && document.querySelector(".chat-conversation") && ${shown === null ? '!document.querySelector(".chat-library")' : `document.querySelector(".chat-library")?.textContent===${JSON.stringify(shown)}`}`);
}
});
test('chat status line: a project with no repository link reads no library status', async t => {
const { b } = await setup(t, { before: 'window.__repo=null;' });
assert.equal(await b.evaluate('window.__statusReads.length'), 0);
assert.equal(await b.evaluate('!!document.querySelector(".chat-library")'), false);
});
// Removing the nested problem display would hide the reason default limits are in force.
test('chat status line: unreadable reply limits show their problem and both fallback keys', async t => {
const limits = { ...STATUS, lastError: null, diverged: null, problems: [],
replyLimits: { requireCitation: false, citationsPerReply: 8,
fallback: ['requireCitation', 'citationsPerReply'], problem: 'reply-limits-unreadable' } };
const { wait, text } = await setup(t, { before: `window.__status={status:200,body:${JSON.stringify(limits)}};` });
await wait('document.querySelector(".chat-library")');
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems');
});
// Comparing commit to last would incorrectly mark the first cite; trusting missing file would
// incorrectly mark the second. The third ensures malformed last never becomes a changed marker.
test('chat replies: unchanged file at a different library commit and incomplete checks stay unmarked', async t => {
const { b, wait } = await setup(t);
await b.evaluate(`window.__checkOverride={citations:[{scope:'canon',agent:null,file:'${C2}',last:'${C2}'},
{scope:'playground',agent:null,last:'${C3}'}, {scope:'supporting',agent:null,file:'${C2}',last:'bad'}]};true`);
await b.evaluate(`const old=window.fetch;window.fetch=async(url,init)=>{const res=await old(url,init);
if(!String(url).includes('/messages'))return res;
const data=await res.json();data.rows[1].check=window.__checkOverride;
return new Response(JSON.stringify(data),{headers:{'content-type':'application/json'}});
};true`);
await wait('!document.querySelector("#refresh").disabled');
await b.evaluate('document.querySelector(".chat-area").dataset.beforeRefresh="yes";document.querySelector("#refresh").click();true');
await wait('!document.querySelector(".chat-area")?.dataset.beforeRefresh && !document.querySelector("#refresh").disabled');
assert.equal(await b.evaluate('document.querySelectorAll(".chat-changed").length'), 0);
});
// Invalid configuration is a diagnostic, unlike an ordinary project with no library mapping.
test('chat status line: an invalid library mapping remains visible for diagnosis', async t => {
const { b, wait, text } = await setup(t, { before: "window.__status={status:502,body:{error:'library-mapping-invalid'}};" });
await wait('window.__statusReads.length===1');
assert.equal(await text('.chat-library [role=status]'), 'Library status unavailable (library-mapping-invalid).');
assert.equal(await b.evaluate('window.__reverts.length'), 0);
});
@@ -1,185 +0,0 @@
// Row 225 (C2-10b, design 7.4): the chat's library status read, a write notice's Revert and the
// shared reply renderer served to the page. The revert is C2-02's `library.revert`; until that verb
// exists the bus refuses it and the route reports the code.
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { readFileSync } from 'node:fs';
import { startServer, userRoute } from '../src/serve.mjs';
import { BusReadError } from '../src/bus.mjs';
const preview = { host: 'console.example', header: 'x-user', user: 'jason' };
const headers = { host: 'console.example', 'x-user': 'jason', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
const C1 = 'a'.repeat(40), C2 = 'b'.repeat(40);
const payload = { project: 'books', commit: C1 };
const STATUS = { project: 'books', version: 1, branch: 'main', served: C2, lastFetch: '2026-10-23T18:00:00.000Z', lastError: null, diverged: null, problems: [] };
async function setup(t, { enabled = true, revert, status, lines = [] } = {}) {
const calls = { revert: [], status: [] };
const server = await startServer({
port: 0, preview, log: l => lines.push(l),
send: enabled ? async () => ({}) : null,
libraryStatus: status === null ? null : async args => { calls.status.push(args); return status ? status(args) : STATUS; },
libraryRevert: enabled ? async args => { calls.revert.push(args); return revert ? revert(args) : { project: args.project, path: 'Notes/Eyes.md', reverted: args.commit, commit: C2 }; } : null,
});
t.after(() => new Promise(r => server.close(r)));
return { calls, lines, port: server.address().port };
}
function call(s, { method = 'POST', path = '/api/library/revert', hs = headers, data = payload } = {}) {
return new Promise((resolve, reject) => {
const req = request({ hostname: '127.0.0.1', port: s.port, path, method, headers: hs }, r => {
let text = '';
r.on('data', c => text += c);
r.on('end', () => resolve({ status: r.statusCode, type: r.headers['content-type'], text, body: text.startsWith('{') ? JSON.parse(text) : null }));
});
req.on('error', reject);
req.end(data === null ? undefined : typeof data === 'string' ? data : JSON.stringify(data));
});
}
const get = (s, path, hs = headers) => call(s, { method: 'GET', path, hs, data: null });
const refusal = code => { throw new BusReadError(code); };
test('a signed-in same-origin revert reaches exactly one library.revert and returns the new commit', async t => {
const s = await setup(t), r = await call(s);
assert.equal(r.status, 200, r.text);
assert.deepEqual(s.calls.revert, [payload]);
assert.deepEqual({ ...r.body, at: undefined }, { project: 'books', path: 'Notes/Eyes.md', reverted: C1, commit: C2, at: undefined });
assert.ok(s.lines.some(l => l.includes('library-revert') && l.includes(`commit=${C2}`) && l.includes('project="books"')));
});
test('a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata', async t => {
const off = await setup(t, { enabled: false });
assert.deepEqual((await call(off)).body, { error: 'read-only', sent: false });
const s = await setup(t);
for (const [key, value, status] of [['host', 'wrong.example', 403], ['x-user', 'other', 401], ['origin', 'https://else.example', 403], ['sec-fetch-site', 'cross-site', 403],
['origin', undefined, 403], ['sec-fetch-site', undefined, 403], ['x-user', ['jason', 'jason'], 401]]) {
const hs = { ...headers };
if (value === undefined) delete hs[key]; else hs[key] = value;
const r = await call(s, { hs });
assert.equal(r.status, status, key);
assert.notEqual(r.body?.sent, true);
}
assert.equal((await call(s, { method: 'GET', data: null })).status, 405);
assert.deepEqual(s.calls.revert, []);
});
test('a revert takes a project and a 40-hex commit, nothing else, before the bus is asked', async t => {
const s = await setup(t);
for (const data of ['not json', 'null', '[]', '5', { project: 'books' }, { commit: C1 }, { ...payload, path: 'a.md' },
{ project: 'books', commit: 'abc' }, { project: 'books', commit: C1.toUpperCase() }, { project: '../x', commit: C1 }, { project: 7, commit: C1 }]) {
const r = await call(s, { data });
assert.equal(r.status, 400, JSON.stringify(data));
assert.equal(r.body.error, 'invalid-request');
assert.equal(r.body.sent, false);
}
assert.deepEqual(s.calls.revert, []);
});
test('a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal', async t => {
const codes = { 'unknown-verb': 502, 'library-revert-not-agent': 409, 'stale-base': 409, 'human-required': 403, 'project-not-configured': 503, 'invalid-request': 400 };
for (const [code, status] of Object.entries(codes)) {
const s = await setup(t, { revert: () => refusal(code) }), r = await call(s);
assert.equal(r.status, status, code);
assert.equal(r.body.error, code);
assert.equal(r.body.sent, false);
assert.match(r.body.message, new RegExp(`not reverted: the bus refused it \\(${code}\\)`));
assert.equal(s.calls.revert.length, 1, 'never retried');
}
});
test('a revert whose answer is lost or unusable is reported unknown and is never retried', async t => {
const answers = [() => refusal('outcome-unknown'), () => { throw new Error('socket closed'); }, () => ({ project: 'books', path: 'a.md', reverted: C1 }),
() => ({ project: 'books', path: 'a.md', reverted: C2, commit: C2 }), () => ({ project: 'books', reverted: C1, commit: C2 }), () => null];
for (const revert of answers) {
const s = await setup(t, { revert }), r = await call(s);
assert.equal(r.status, 503, revert.toString());
assert.equal(r.body.error, 'outcome-unknown');
assert.equal(r.body.sent, 'unknown');
assert.equal(s.calls.revert.length, 1);
}
});
test('the status read takes one project query and answers the bus result for that project', async t => {
const s = await setup(t), r = await get(s, '/api/library/status?project=books');
assert.equal(r.status, 200, r.text);
assert.deepEqual(s.calls.status, [{ project: 'books' }]);
assert.deepEqual({ ...r.body, at: undefined }, { ...STATUS, at: undefined });
for (const path of ['/api/library/status', '/api/library/status?project=books&x=1', '/api/library/status?project=books&project=other', '/api/library/status?project=../x'])
assert.equal((await get(s, path)).status, 400, path);
assert.equal((await call(s, { path: '/api/library/status?project=books' })).status, 403, 'the preview write fence precedes GET-only routes');
assert.equal(s.calls.status.length, 1);
});
test('a status answer for another project or of the wrong shape is refused; a refusal keeps its status', async t => {
for (const answer of [{ ...STATUS, project: 'other' }, null, [], 'books']) {
const s = await setup(t, { status: () => answer }), r = await get(s, '/api/library/status?project=books');
assert.equal(r.status, 502, JSON.stringify(answer));
assert.equal(r.body.error, 'invalid-response');
}
for (const [code, status] of Object.entries({ 'human-required': 403, 'project-not-configured': 503, 'project-not-declared': 400, 'library-mapping-invalid': 502 })) {
const s = await setup(t, { status: () => refusal(code) }), r = await get(s, '/api/library/status?project=books');
assert.equal(r.status, status, code);
assert.equal(r.body.error, code);
}
const none = await setup(t, { status: null });
assert.equal((await get(none, '/api/library/status?project=books')).body.error, 'not-configured');
});
test('the status read needs no send; a read-only Console still has no revert', async t => {
const s = await setup(t, { enabled: false });
assert.equal((await get(s, '/api/library/status?project=books')).status, 200);
assert.equal((await call(s)).status, 403);
});
test('library routes are the owner\'s: a granted user is refused them, and the tester never reverts', async t => {
for (const [path, method] of [['/api/library/status', 'GET'], ['/api/library/revert', 'POST']]) assert.equal(userRoute(path, method), false, path);
await assert.rejects(startServer({ port: 0, tester: true, testerSocket: '/nonexistent', libraryRevert: async () => ({}) }), /tester listener permits only/);
});
test('the page gets the one library renderer, byte for byte, as a JavaScript module', async t => {
const s = await setup(t), r = await get(s, '/render.mjs');
assert.equal(r.status, 200);
assert.match(r.type, /^text\/javascript/);
assert.equal(r.text, readFileSync(new URL('../../library/src/render.mjs', import.meta.url), 'utf8'));
});
// A valid commit from an unrelated project is an unusable receipt, never evidence of this revert.
test('a revert receipt for another project is unknown and is never retried', async t => {
const s = await setup(t, { revert: args => ({ project: 'other', path: 'Notes/Eyes.md', reverted: args.commit, commit: C2 }) });
const r = await call(s);
assert.equal(r.status, 503);
assert.equal(r.body.error, 'outcome-unknown');
assert.equal(r.body.sent, 'unknown');
assert.deepEqual(s.calls.revert, [payload]);
});
// Catch a route being added ahead of the granted-user owner fence: neither transport may be used.
test('a granted-user HTTP session cannot reach owner library callbacks', async t => {
let ownerCalls = 0, userCalls = 0;
const sso = { publicHost: 'console.example', publicOrigin: 'https://console.example',
formTargets: () => [], session: () => ({ name: 'Alice', user: 'alice' }) };
const server = await startServer({ port: 0, sso, send: async () => ({}),
libraryStatus: async () => { ownerCalls++; return STATUS; },
libraryRevert: async () => { ownerCalls++; return { project: 'books', path: 'a.md', reverted: C1, commit: C2 }; },
userOptions: () => { userCalls++; return {}; }, log: () => {} });
t.after(() => new Promise(r => server.close(r)));
const s = { port: server.address().port };
for (const options of [{ method: 'GET', path: '/api/library/status?project=books', data: null }, {}]) {
const r = await call(s, options);
assert.equal(r.status, 403);
assert.equal(r.body.error, 'owner-only');
}
assert.equal(ownerCalls, 0);
assert.equal(userCalls, 0);
});
// A supplied callback is insufficient authority: --send must independently be enabled.
test('a configured revert callback remains unreachable when send is off', async t => {
let calls = 0;
const server = await startServer({ port: 0, preview,
libraryRevert: async () => { calls++; return { project: 'books', path: 'a.md', reverted: C1, commit: C2 }; } });
t.after(() => new Promise(r => server.close(r)));
const r = await call({ port: server.address().port });
assert.equal(r.status, 403);
assert.equal(r.body.error, 'read-only');
assert.equal(r.body.sent, false);
assert.equal(calls, 0);
});
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# FED R7 / #1802. Scratch acceptance with labelled ancestry stand-in (sage-3625).
# No systemd units. All children run in owned process groups, removed on exit.
set -euo pipefail
R7_INTERRUPT=0
case "${1:-}" in
'') ;;
--kill-after-start) R7_INTERRUPT=1 ;;
*) echo 'usage: scripts/test-federation-acceptance.sh [--kill-after-start]' >&2; exit 2 ;;
esac
[ "$#" -le 1 ] || exit 2
for R7_TOOL in ss rg timeout node ps awk; do
command -v "$R7_TOOL" >/dev/null || { echo "R7 requires $R7_TOOL" >&2; exit 1; }
done
cd "$(dirname "${BASH_SOURCE[0]}")/.."
R7_ROOT="$(mktemp -d /mnt/storage/scratch/tmp/r7.XXXXXXXX)"
mkdir -p "$R7_ROOT/home" "$R7_ROOT/tmp"
touch "$R7_ROOT/groups" "$R7_ROOT/ports" "$R7_ROOT/sockets"
cleanup() {
local result=$? pid start raw port socket listeners cleanup_failed=0 attempt
local -a fields
trap - EXIT INT TERM
if [ -n "${R7_DRIVER:-}" ]; then
kill -TERM "$R7_DRIVER" 2>/dev/null || true
wait "$R7_DRIVER" 2>/dev/null || true
fi
# A detached child is its group's leader. Confirm pid/start time/group before
# signalling only groups in this run's ledger; never a live stack process.
while read -r pid start; do
[ -n "$pid" ] || continue
if [ -r "/proc/$pid/stat" ]; then
raw="$(cat "/proc/$pid/stat")"
read -r -a fields <<< "${raw##*) }"
if [ "${fields[19]}" != "$start" ] || [ "${fields[2]}" != "$pid" ]; then
echo "R7 cleanup failed: group identity changed: $pid" >&2
result=1; cleanup_failed=1
continue
fi
kill -TERM -- "-$pid" 2>/dev/null || true
for ((attempt=0; attempt<800; attempt++)); do
ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break
sleep 0.05
done
kill -KILL -- "-$pid" 2>/dev/null || true
for ((attempt=0; attempt<40; attempt++)); do
ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break
sleep 0.05
done
fi
# Zombies hold no tools/listeners and are reaped by their parent. The test
# requires no live member of an owned group, including orphan descendants.
if ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}'; then
echo "R7 cleanup failed: group $pid still has a live process" >&2
result=1; cleanup_failed=1
fi
done < "$R7_ROOT/groups"
while IFS= read -r port; do
[ -n "$port" ] || continue
if ! listeners="$(ss -H -ltn "sport = :$port")"; then
echo "R7 cleanup failed: cannot inspect port $port" >&2
result=1; cleanup_failed=1
elif [ -n "$listeners" ]; then
echo "R7 cleanup failed: port $port still listens" >&2
result=1; cleanup_failed=1
fi
done < "$R7_ROOT/ports"
while IFS= read -r socket; do
[ -n "$socket" ] || continue
if ! listeners="$(ss -H -lx)"; then
echo "R7 cleanup failed: cannot inspect socket $socket" >&2
result=1; cleanup_failed=1
elif printf '%s\n' "$listeners" | rg -F -- "$socket" >/dev/null; then
echo "R7 cleanup failed: socket $socket still listens" >&2
result=1; cleanup_failed=1
fi
done < "$R7_ROOT/sockets"
if listeners="$(ss -H -lx)"; then
if printf '%s\n' "$listeners" | rg -F -- "$R7_ROOT/" >/dev/null; then
echo 'R7 cleanup failed: another scratch Unix listener remains' >&2
result=1; cleanup_failed=1
fi
else
result=1; cleanup_failed=1
fi
if [ "$result" -ne 0 ]; then
for raw in "$R7_ROOT"/*.log; do
[ -f "$raw" ] || continue
echo "R7 diagnostic: ${raw##*/}" >&2
tail -40 "$raw" >&2
done
fi
rm -rf -- "$R7_ROOT"
if [ -e "$R7_ROOT" ]; then
echo 'R7 cleanup failed: scratch directory remains' >&2
result=1; cleanup_failed=1
fi
if [ "$cleanup_failed" -eq 0 ]; then echo 'R7 cleanup: no child groups, ports or sockets remain; no units created'; fi
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
# No host configuration, preload hooks, credentials or agent env in fixtures.
# The ancestry stand-in lives only in federation-host.mjs, per sage-3625.
timeout -k 10 210 env -i PATH="$PATH" HOME="$R7_ROOT/home" TMPDIR="$R7_ROOT/tmp" \
MOSAIC_R7_ROOT="$R7_ROOT" MOSAIC_R7_INTERRUPT="$R7_INTERRUPT" \
node --test --test-concurrency=1 packages/cli/tests/federation-acceptance.test.mjs &
R7_DRIVER=$!
wait "$R7_DRIVER"
R7_DRIVER=