Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94128f73f7 | ||
|
|
5079012771 | ||
|
|
77172ce3b4 | ||
|
|
1afcfc766d | ||
|
|
4732821f7a | ||
|
|
8d37d21cb8 | ||
|
|
9a131e9567 | ||
|
|
66f7becf21 |
No files matched your search
+3
-9
@@ -5412,14 +5412,8 @@ 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 | Gladstone | Gitea #1816 / Vikunja 614 hostcheck item 5 and bundle verification | Before: dpkg's correct 750 form falsely fails item 5, and two run-sheet bundle verifies require a missing repository. Draft after: exact root/mosaic/path with octal 0750 equivalence; stat/output unchanged; both verifies use cleaned-up bare scratch repos. Three additive-only test blocks, two planned mutants. Untested under Sage-3738; Sage-3740 authorizes untested branch push for static review. Queue row pending; Sage owns gate and landing.
|
||||
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 | Gladstone | #1816 / Vikunja 614 scope amendment mos-1534, sage-3741 | F1 narrowed to exact 750/0750 and single spaces; F3 allows @ in absolute unit paths with unsafe-character refusals retained; F4 moves copy_tree to / after path resolution. AS_USER audit found git -C and stack-release's existing checkout cd cover the other steps. Added new unit and unreadable-cwd copy tests only. Untested under Sage's no-Node hold; no live action.
|
||||
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 | Gladstone | #1816 / Vikunja 614 F5 draft (mos-1536) | Before: the run sheet omits the explicit notify decision and precheck misses a bus-start refusal. After: section 9a creates a private null-binding decision as mosaic; precheck directly reuses readNotifyConfig and private_dir, and new tests cover accepted and refused decisions. Existing fixture setup additions await Mos approval through Sage; installer unchanged. Untested under no-Node hold.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 fixture ruling mos-1537 | Added only three setup lines each to host(t)/world(t) for the explicit null notifier decision. Existing assertions/fakes/lines intact. New install test checks decision bytes/mode preserved and missing decision remains absent. F5 missing-file fault removes the setup file. Untested under Sage's hold; separate follow-up atop 33950cf9b.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 F6 and notify parent (mos-1537) | Before: the run sheet's copy check lacks its required CLI wrapper; mkdir -p -m does not guarantee the notify parent mode. After: a separately Mos-gated wrapper --create step follows install and precedes check, as mosaic from cwd /; section 9a explicitly creates both notify directories at 0700. Docs only, no live action; Node/tests still held.
|
||||
|
||||
2026-10-23 | Gladstone | Row320 / #1816 first review CHANGES (morgana-0217) | Reviewer run: hostcheck19/19, unit47/47, install24/24, copy17/18; seven of eight mutants killed, M4 survived because both base/mutant failed only cleanup. Fixed this row's F4 test to enter cwd then chmod000, assert no read/search, pass an absolute outside bundle, and restore0700 in finally. Existing tests/setup and production scripts unchanged. Correction untested locally pending Morgana's focused rerun; no acceptance 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.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Beakley
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 217 (C2-06, handed over from Launchpad).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 88b97f65) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `beakley/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1,8 +0,0 @@
|
||||
# Boyd
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 298 (the chief-of-staff grant scope, handed over from Megavolt).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 48a3ae38) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `boyd/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1,8 +0,0 @@
|
||||
# Bubba
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 288 (agent write notices and library revert, handed over from Honker).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 3d6672b5) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `bubba/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1,30 +0,0 @@
|
||||
# Row 239 round 1 verdict: honker/row239-r4 e0cb1db2c (duckworth-0295)
|
||||
|
||||
Request: honker-0206 (in-review round 1, rev 3719). Candidate: e0cb1db2c on refactor 546366f4a.
|
||||
Method: git reads of local objects only. No node, no tests, no fetch, no queue reads.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- **The replay is exact.** `git range-diff 93694c518..5b8492b0f 546366f4a..0cae2dad5` gives four `=`
|
||||
lines. None of the 16 manifest paths changed between 93694c518 and 546366f4a, and
|
||||
`git diff 5b8492b0f 0cae2dad5` over those 16 paths is empty. My r3 read (REBASE-r3-5b8492b0f.md,
|
||||
duckworth-0292) applies unchanged, and so do the 0165 design approval and the 0168 code approval with
|
||||
C1 and C2 met.
|
||||
- **e0cb1db2c is doc only.** It appends 26 lines to agents/honker/work/queue-239/BUILD.md. No test reads
|
||||
that file.
|
||||
- **Pins at e0cb1db2c.** All four 64-hex pins in channel-proof.test.mjs match the files' sha256:
|
||||
human.mjs, human-cli.mjs, transport.mjs and webui bus.mjs (e3f52172…). Upstream changed library
|
||||
src/index.mjs and business src/index.mjs, and no test or script pins them. The only hit in my grep
|
||||
is a basename collision (`cli.mjs` matched `human-cli.mjs`).
|
||||
- **The run, as Honker reported it (sage-3773 CLEAR, on 0cae2dad5. I didn't rerun it):**
|
||||
- The six touched test files ran whole: 65/65, 0 skipped, no first-run red.
|
||||
- Mutants M1-M3g: 9/9 killed as assertion failures in their named tests.
|
||||
- M3f killed T6, which matches my static trace in 0292.
|
||||
- Every restore matched by sha256, and the tree was clean afterwards.
|
||||
Running on the replay rather than r3's base was right, because bus runtime.mjs imports library
|
||||
src/index.mjs and the cli imports business src/index.mjs, and both changed upstream.
|
||||
- **What remains is Sage's.** The full gate runs on the replay at landing (sage-3730). Sage's call.
|
||||
|
||||
## Notes
|
||||
|
||||
- None new. 0168 N1 (endsWith access-revoked) stands as recorded.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Fethry
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 225 (C2-10b, handed over from Morgana).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 14f80e3c) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `fethry/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,376 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,64 @@
|
||||
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
|
||||
@@ -0,0 +1,64 @@
|
||||
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
|
||||
@@ -0,0 +1,16 @@
|
||||
✔ 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
|
||||
@@ -0,0 +1,78 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
@@ -0,0 +1,496 @@
|
||||
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
|
||||
@@ -0,0 +1,48 @@
|
||||
✔ 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'
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,192 @@
|
||||
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
|
||||
@@ -0,0 +1,206 @@
|
||||
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
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
@@ -0,0 +1,74 @@
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,192 @@
|
||||
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
|
||||
@@ -0,0 +1,206 @@
|
||||
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
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
@@ -0,0 +1,81 @@
|
||||
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
|
||||
@@ -0,0 +1,81 @@
|
||||
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
|
||||
@@ -0,0 +1,102 @@
|
||||
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
|
||||
@@ -0,0 +1,83 @@
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
@@ -0,0 +1,106 @@
|
||||
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
|
||||
@@ -0,0 +1,177 @@
|
||||
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
|
||||
@@ -0,0 +1,126 @@
|
||||
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
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
✔ 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
|
||||
@@ -0,0 +1,17 @@
|
||||
✔ 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
|
||||
@@ -0,0 +1,88 @@
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
✔ 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
|
||||
@@ -0,0 +1,76 @@
|
||||
✔ 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'
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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.' });
|
||||
+});
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/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()
|
||||
@@ -1,9 +0,0 @@
|
||||
# Gladstone
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos (mos-1531, mos-1532). Owns the Case 1/2 host fixes for the 433 P1 rerun,
|
||||
F1 to F6 (#1816, Vikunja 614).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 433058fc) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `gladstone/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1,304 +0,0 @@
|
||||
# Hostcheck item 5 and bundle verification (Gitea #1816, Vikunja 614)
|
||||
|
||||
Queue row pending from Sage. Owner Gladstone, reviewer Morgana, gate/landing Sage.
|
||||
Base: fetched origin/refactor 93694c5185d0a7f989d2fe78fb620e44599b744e;
|
||||
branch gladstone/hostcheck-item5. Scope: mos-1531 / ops-0256, sage-3738 and sage-3740.
|
||||
|
||||
## Before and after
|
||||
|
||||
Ubuntu 24.04 dpkg 1.22.6ubuntu6.6 prints `root mosaic 750 /usr/bin/bwrap`.
|
||||
Item 5 compared the literal `0750`, falsely refusing a correctly configured host;
|
||||
--enforce would unload the profile, close bwrap and record DRIFT.
|
||||
The candidate accepts exactly four fields, root, mosaic, octal 0750, and $BWRAP.
|
||||
`0*750` represents all octal spellings numerically equal to 0750 without arithmetic
|
||||
conversion/overflow. Horizontal whitespace separates fields; extra fields and
|
||||
embedded newlines refuse. Owner, group and path remain exact. The stat comparison
|
||||
and existing output strings remain unchanged. Only the item 5 comment also changes.
|
||||
|
||||
The two release-transport bundle checks previously ran outside any repository.
|
||||
Each now creates an empty bare scratch repository, verifies with git -C, and removes
|
||||
the scratch directory using an EXIT trap. A failure exits the shell before cloning
|
||||
or fetching. No other run-sheet text changes.
|
||||
|
||||
## Tests and mutants
|
||||
|
||||
Only packages/cli/tests/case1-hostcheck.test.mjs changes among tests, by appending
|
||||
three new test() blocks; the existing passing 0750 fake and failing fake are intact.
|
||||
The pass test exercises 750, 0750 and 000750. The enforce test checks no unload,
|
||||
chown, chmod, DRIFT or hostcheck log for 750. Refusals independently cover 755,
|
||||
0755, 4750, 75, 7500, 075, non-octal strings, other owner/group/path, missing path,
|
||||
extra fields, multiple lines, blank/empty output and a missing fixture file.
|
||||
|
||||
| Mutant | Change | Killer | Result |
|
||||
|---|---|---|---|
|
||||
| M1 | Restore the old literal root mosaic 0750 comparison (drop 750) | new check pass test | Unrun |
|
||||
| M2 | Accept any mode while preserving owner/group/path | new refusal test | Unrun |
|
||||
|
||||
Mutant procedure: preserve script bytes, assert one replacement, run the named
|
||||
killer alone, require failure for its intended assertion, restore in finally and
|
||||
check the original bytes/hash. Run the original targeted file again after restore.
|
||||
|
||||
## Verification status
|
||||
|
||||
Untested. Sage-3738 prohibits all Node until CLEAR because the joint full gate
|
||||
runs alone. Sage-3740 explicitly directs pushing this untested build for Morgana's
|
||||
static review before CLEAR. This supersedes test-first execution for this draft;
|
||||
no Node, tests, node --check, full suites or live-host operations have run.
|
||||
Queue next refused linked-worktree execution; Sage owns queue creation/movement.
|
||||
Push only gladstone/*, with MOSAIC_GITEA_CREDENTIAL_FILE unset; no amend/force.
|
||||
|
||||
Static draft receipt: `git diff --check` passed. Test-file numstat against the
|
||||
base is `46 0 packages/cli/tests/case1-hostcheck.test.mjs` (added/deleted).
|
||||
|
||||
## Scope and implementation amendment (mos-1534, sage-3741)
|
||||
|
||||
F1 correction: Sage requires precisely `750` or `0750` and exactly single spaces,
|
||||
so the candidate now uses `0?750` and literal spaces. It refuses 00750/000750,
|
||||
a doubled space and a tab. This supersedes the initial numeric-equivalence design
|
||||
above. Only this row's own appended tests change; no base test line changes.
|
||||
|
||||
F3: case2-unit.sh plain() adds @. abs_plain is its only caller and requires an
|
||||
absolute normalized path, so @ cannot become an ExecStart prefix. In sed @ is
|
||||
ordinary replacement text (the delimiter is |); &, backslash and | still refuse.
|
||||
Environment=MOSAIC_CLAUDE_BIN carries @ literally; the parser compares the complete
|
||||
value, with no shell evaluation. @anthropic-ai also cannot match the residual
|
||||
@[A-Z_]+@ token check. Tests render/reinstall with a regular executable under
|
||||
@anthropic-ai, checking Environment and the new copy's WorkingDirectory. Since a
|
||||
nonroot test cannot create root ownership, a stat fake models root:0755 only for
|
||||
that file and its ancestor chain. The real file type/executable checks, render,
|
||||
installed-unit parser, and all other stat calls remain active. A separate block
|
||||
refuses space, %, $, both quote characters, backslash, & and |.
|
||||
|
||||
F4: copy_tree changes to / before archive/npm/find, after all its path arguments
|
||||
are absolute. copy_bundle retains its existing relative-bundle resolution before
|
||||
calling copy_tree. Checkout copies have absolute REPO and COPIES too. Source-only
|
||||
use from stack-release.sh changes no cwd because copy_tree is not called there.
|
||||
Audit of every AS_USER command in stack-upgrade.sh: fetch and merge use git -C;
|
||||
plan, gate, revert, confirm-rollback, stage (activate/rollback), confirm and rehearse
|
||||
invoke stack-release.sh, which changes to its checkout at line 34. No other
|
||||
AS_USER command needs a fix. The new test enters a directory mode 0111, verifies
|
||||
that ls cannot read it, then runs both a relative-bundle copy and checkout copy,
|
||||
asserting success, no find restore error, and an immutable completed copy.
|
||||
|
||||
Additional mutants (unrun under the same no-Node hold):
|
||||
|
||||
| Mutant | Change | Killer | Result |
|
||||
|---|---|---|---|
|
||||
| M3 | Drop @ from plain() | new factory scoped Claude-path test | Unrun |
|
||||
| M4 | Remove copy_tree's cd / | new unreadable-cwd copy test | Unrun |
|
||||
|
||||
All four fixes remain untested. Follow-up is an additive commit on the pushed
|
||||
branch, per sage-3741, with no amend or force. Morgana's static F1/F2 read of
|
||||
f30c74b12 was CLEAN (morgana-0205); F3/F4 and execution remain open.
|
||||
|
||||
Morgana N1: the outer bundle-verification failure now prints
|
||||
`STOP: bundle verify failed; do not continue` instead of exiting the interactive
|
||||
root shell. The subshell still returns failure and cleans up. This follows the
|
||||
sheet's human read-and-stop convention and avoids sending remaining pasted lines
|
||||
to a parent shell after exiting sudo -i. Inner mktemp failure still exits only the
|
||||
subshell. This supersedes the initial outer-shell failure behavior noted above.
|
||||
|
||||
## File numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 4 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 114 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 12 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 29 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## F5 (mos-1536 / ops-0260): explicit notifier decision
|
||||
|
||||
Before: no run-sheet step creates the required notifier decision, and precheck
|
||||
accepts a host whose bus will refuse missing notify.json. After: section 9a
|
||||
requires the mosaic user to create its business notify directory at 0700 and an
|
||||
explicit {"notifyVersion": 1, "binding": null} file at 0600 before section 10.
|
||||
The shell runs with cwd / and umask 077; noclobber refuses an existing decision.
|
||||
Named binding is explicitly a later per-host choice requiring Mos's go.
|
||||
case2-install.sh is unchanged and makes no silent default.
|
||||
|
||||
case2-precheck.sh's existing facts helper dynamically imports readNotifyConfig
|
||||
from cli.mjs and calls it directly. File type, uid, exact 0600 mode, JSON keys,
|
||||
version and binding-name validation therefore stay identical to the bus. The
|
||||
shell checks the business notify directory with existing private_dir (same uid,
|
||||
not a symlink, exactly 0700) and prints the new notifier config line. No notifier
|
||||
is started and no configuration is written. Tests append pass cases for null and
|
||||
a valid named binding, and refusal cases for missing file, 0644, 0755 directory,
|
||||
extra key, malformed JSON, invalid binding/version and a symlinked file.
|
||||
|
||||
M5 (unrun): remove the notifier shell check, including its refusal branch; the
|
||||
new notifier refusal test must fail. Also include Morgana's F3 negative mutant
|
||||
M3b (allow $ in plain), killed by the new unsafe-character test.
|
||||
|
||||
Existing positive precheck tests need an explicit notify fixture. Approval
|
||||
requested via Sage in gladstone-0006/-0007 for minimal setup-only additions to
|
||||
case2-unit.test.mjs host(t) and case2-install.test.mjs world(t); assertions remain
|
||||
unchanged. Those additions are not yet applied. Correction to gladstone-0007:
|
||||
case1-accept.test.mjs also names case2-precheck.sh, but uses a stand-in; it does
|
||||
not execute the changed script and needs no fixture amendment.
|
||||
|
||||
Execution remains held by Sage: no Node/tests run. Morgana-0207 static F1-F4
|
||||
verdict on a75532db1 is CLEAN; execution and F5 review are still open.
|
||||
|
||||
F5 acceptance from mos-1536 (for review):
|
||||
1. Before section 10, as mosaic, explicitly create <dataRoot>/notify/<business>
|
||||
at 0700 and notify.json at 0600 with default binding null; a later named
|
||||
binding needs Mos's go. Do not make case2-install.sh write it.
|
||||
2. Precheck must refuse missing file, non-0600 file, directory looser than 0700
|
||||
and a document outside the CLI's exact shape. Reuse/match cli.mjs validation,
|
||||
and name the new line in this run-sheet step's Precheck entry.
|
||||
3. Append-only test blocks cover pass, missing, 0644, 0755 dir, extra key; record
|
||||
a skip-check mutant, the touched test file and its numstat.
|
||||
4. F3/F4 precede F5, each follow-up commit stays on gladstone/hostcheck-item5;
|
||||
send pushes to Sage/Morgana with Mos cc. No Node until Sage CLEAR.
|
||||
|
||||
Static F5 draft receipt: git diff --check passed. Existing fixture approvals
|
||||
remain pending; F5's new pass test creates its own explicit decisions. No claim
|
||||
that the old positive precheck tests pass without the pending setup changes.
|
||||
|
||||
## F5 candidate file numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 6 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 179 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 34 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 65 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## Fixture approval and separate follow-up (mos-1537)
|
||||
|
||||
Mos approved exactly setup-only additions to case2-unit.test.mjs host(t) and
|
||||
case2-install.test.mjs world(t): each gets three added lines for a 0700
|
||||
notify/acme directory and 0600 notify.json with {"notifyVersion":1,"binding":null}.
|
||||
No existing line, fake or assertion changes or is deleted. F5's missing-file
|
||||
negative case removes the file after setup, so the new default cannot mask it.
|
||||
The newly appended install test checks preserved bytes and mode after success,
|
||||
and separately removes the decision before install and checks it stays absent.
|
||||
case1-accept.test.mjs is unchanged. This follow-up is committed separately on
|
||||
33950cf9b as directed, with no amend. No Node/tests run; suite claims remain open.
|
||||
Morgana-0211 found F5 source/run sheet CLEAN statically; the setup prerequisite B1
|
||||
is addressed by this approved fixture patch, pending execution.
|
||||
|
||||
Additional planned mutants from Morgana-0211 (unrun): M5b drops private_dir,
|
||||
killed by the directory fault; M5c prints NOTIFY ok unconditionally, killed by
|
||||
the document faults. Existing test preservation is checked by git diff showing
|
||||
only additions (numstat below).
|
||||
|
||||
Fixture follow-up numstat: case2-unit.test.mjs +3/-0,
|
||||
case2-install.test.mjs +18/-0. Against the original base: case1-hostcheck.test.mjs
|
||||
+48/-0, case2-unit.test.mjs +68/-0, stack-copy.test.mjs +16/-0,
|
||||
case2-install.test.mjs +18/-0. git diff --check passed.
|
||||
|
||||
## F6 and notify parent correction (mos-1537 / ops-0261)
|
||||
|
||||
Before: install creates a copy but no human CLI wrapper; the sheet's
|
||||
stack-copy.sh check --units external therefore fails even with correct units.
|
||||
After: the sheet explicitly places a wrapper step after successful install and
|
||||
before that check. On Mos's separate live-step go, mosaic changes cwd to /,
|
||||
creates $HOME/.npm-global/bin if absent, then invokes its clone's stack-copy.sh
|
||||
wrapper <sha> --create. Existing wrappers refuse and require review; install is
|
||||
unchanged. This is documentation of a gated host action, not execution of it.
|
||||
There are no tests pinning these sheet steps to adjust.
|
||||
|
||||
Section 9a now explicitly creates/chmods <dataRoot>/notify and then its business
|
||||
leaf, each 0700. This addresses ops-0261's observation that mkdir -p -m only
|
||||
controls the leaf. The existing umask 077 remains. All exits are inside the
|
||||
unprivileged shell, so no failure exits the operator's interactive root shell.
|
||||
|
||||
Static receipt: git diff --check passed. No Node, tests or live action. F6 and
|
||||
this parent-directory documentation correction are a separate commit after the
|
||||
approved fixture-only 5fd2328b6 follow-up. Reviewer should retain the earlier
|
||||
F1-F5 source verdicts and inspect this delta. Queue row still pending from Sage.
|
||||
|
||||
## F6 candidate file numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 10 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 241 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 51 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-install.test.mjs` | 18 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 68 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## F6 gate citation (sage-3747)
|
||||
|
||||
Added scripts/stack-copy.sh:3 to the F6 wrapper step's explicit Mos-go requirement.
|
||||
This is doc-only; commands and tests are unchanged. Sage ruled the delete-after-
|
||||
setup missing-file negative meets mos-1537, so no fixture change is needed.
|
||||
Morgana-0213 found the fixture/F6 deltas CLEAN statically. No Node/tests run;
|
||||
Morgana's eventual run waits for Sage's row and CLEAR after the 220+216 landing.
|
||||
Numstat for this follow-up: host-run-sheet.md +1/-1; BUILD.md +9/-0.
|
||||
Static receipt: git diff --check passed.
|
||||
|
||||
Correction: 0f3d874d4 added 10 BUILD.md lines, not 9; the git numstat is authoritative.
|
||||
|
||||
## First review run and F4 test correction (morgana-0217)
|
||||
|
||||
Morgana ran af201b4a9 (6fa47e07d only relocates this file) under her authorized
|
||||
slot with timeout -k 10 and TMPDIR under /mnt/storage/scratch/tmp. Receipts:
|
||||
/mnt/storage/scratch/morgana-g320-out/base-summary.txt and mutants-summary.txt.
|
||||
|
||||
| Targeted file | Passed | Failed |
|
||||
|---|---:|---:|
|
||||
| case1-hostcheck.test.mjs | 19 | 0 |
|
||||
| case2-unit.test.mjs | 47 | 0 |
|
||||
| case2-install.test.mjs | 24 | 0 |
|
||||
| stack-copy.test.mjs | 17 | 1 |
|
||||
|
||||
Seven of eight mutants were killed: M1 literal0750, M2 anymode, M3a drop@,
|
||||
M3b allow$, M5 drop shell block, M5b drop private_dir, M5c always NOTIFY ok.
|
||||
M4 drop cd / SURVIVED: base and mutant both failed only setup's cleanup hook.
|
||||
This is a test defect, not a green acceptance result. The 0111 cwd retained
|
||||
search permission and did not reproduce the inaccessible inherited cwd. It
|
||||
also blocked setup's chmod -R cleanup because the directory remained unreadable.
|
||||
|
||||
B1 correction touches only this row's appended F4 test: the child shell enters
|
||||
an initially searchable directory, chmods it 000, checks both ! -r and ! -x, then
|
||||
execs stack-copy. The parent asserts all read/search bits remain cleared. The
|
||||
bundle now lives outside cwd and is passed absolutely, matching the real flow;
|
||||
existing row232 coverage still tests relative bundles from an accessible cwd.
|
||||
Both bundle and checkout paths remain covered.
|
||||
B2 correction: each child run/assertion sequence has a finally that restores the
|
||||
cwd to0700 before setup's existing cleanup. No old setup/test lines change.
|
||||
Production scripts are unchanged; the cd / fix remains correct by static review.
|
||||
|
||||
Correction is untested locally: no builder Node/test CLEAR was given. Morgana
|
||||
will rerun stack-copy whole, M4 and residue cleanup on the pushed correction;
|
||||
other files need no rerun unless changed. git diff --check passed.
|
||||
|
||||
## F4 corrected candidate numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 12 | 0 |
|
||||
| `agents/gladstone/work/queue-320/BUILD.md` | 304 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 51 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-install.test.mjs` | 18 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 68 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 22 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
@@ -1,171 +0,0 @@
|
||||
# Row 321: F7 wrapper environment and F10 first adoption (#1817)
|
||||
|
||||
Owner Gladstone; reviewer Boyd (separate Codex thread); queue/review/landing Sage.
|
||||
Authorized by sage-3791. Branch gladstone/row321 starts at fetched origin/refactor
|
||||
833ec1cc8d7c11842efb82e57c1a0b256c8d22d7. The row320 branch is unchanged.
|
||||
Brief: docs/plans/2026-10-23_host-fixes-1816-brief.md, section
|
||||
"Brief: F7, the wrapper step's PATH and umask under sudo", including F10;
|
||||
verified blob 7333f94ba9cfd58f87c5a3580d428b84c65845b6 (requested pin 7333f94ba9cf).
|
||||
Only the run sheet and this BUILD.md change. No scripts, tests, config, credentials,
|
||||
queue state, deployment or live host operation changed. No tests or Node commands run.
|
||||
|
||||
## Before and after
|
||||
|
||||
F7: sudo secure_path omits the pinned Node directory, so wrapper creation reaches
|
||||
instance.sh's bare node and exits127. An inherited umask can leave the bin dir0775.
|
||||
The MOSAIC_WRAPPER heredoc now sets umask077 and the sheet's existing pinned Node
|
||||
PATH before mkdir. One sentence names secure_path and why the step sets both.
|
||||
The existing Mos go remains explicit. No wrapper command is executed here.
|
||||
|
||||
F10: first install omits adoption, so the first activate refuses no-pointer.
|
||||
The release-transport section now places explicit adoption after both units are
|
||||
running (section10) and before any activate/upgrade, run as mosaic on Mos's go by
|
||||
Jason or Mos, citing stack-release.sh:14. It uses the configured dev instance and
|
||||
external units, with the same pinned Node PATH under sudo.
|
||||
Adjacent to the upgrade command, the sheet now says only the first upgrade after
|
||||
adopt takes --no-rehearsal: adopt writes no backup; missing flag refuses no-backup;
|
||||
later upgrades omit it, because a backup with that flag refuses rehearsal-required.
|
||||
Source checks: stack-release.sh pointer_for_business refuses no-pointer and directs
|
||||
adopt; stack-upgrade.sh rehearse handles rc3/no-backup and rc0/rehearsal-required.
|
||||
This records existing behavior and authority; it adds no requirement or live go.
|
||||
|
||||
## Test-reference grep (negative and positive control)
|
||||
|
||||
The required negative grep covers both F7 and F10 because neither is read by any
|
||||
matched test. The positive control locates the precheck path that the unit/install
|
||||
tests actually execute, showing the pathspecs reach their test files.
|
||||
|
||||
Negative grep:
|
||||
|
||||
```sh
|
||||
git grep -n 'host-run-sheet' -- 'packages/*/tests/*' 'scripts/test-*.sh'
|
||||
# exit 1
|
||||
# no matches
|
||||
```
|
||||
|
||||
Positive control:
|
||||
|
||||
```sh
|
||||
git grep -n 'case2-precheck.sh' -- 'packages/*/tests/*' 'scripts/test-*.sh'
|
||||
# exit 0
|
||||
packages/cli/tests/case1-accept.test.mjs:26:const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
|
||||
packages/cli/tests/case2-install.test.mjs:2:// scripts/case2-precheck.sh's library checks and --library-status mode. Brief:
|
||||
packages/cli/tests/case2-install.test.mjs:23:const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
packages/cli/tests/case2-install.test.mjs:271:// ---- case2-precheck.sh, the library profile (D6a) ----
|
||||
packages/cli/tests/case2-install.test.mjs:334:// ---- case2-precheck.sh --library-status (D6b) ----
|
||||
packages/cli/tests/case2-unit.test.mjs:2:// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
|
||||
packages/cli/tests/case2-unit.test.mjs:20:const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
packages/cli/tests/case2-unit.test.mjs:588:// ---- case2-precheck.sh ----
|
||||
```
|
||||
|
||||
## Run-sheet diff against the pinned base (git diff --unified=0)
|
||||
|
||||
```diff
|
||||
diff --git a/docs/guides/host-run-sheet.md b/docs/guides/host-run-sheet.md
|
||||
index d2ba28d7c..96fd4f363 100644
|
||||
--- a/docs/guides/host-run-sheet.md
|
||||
+++ b/docs/guides/host-run-sheet.md
|
||||
@@ -209,0 +210,9 @@ sudo -u mosaic -H git clone -q -b refactor /srv/mosaic/incoming/<sha>.bundle /sr
|
||||
+After a first install, once both units are running (section 10), adopt the installed copy before
|
||||
+any activate or upgrade. This step runs only on Mos's go, with Jason or Mos running it
|
||||
+(`scripts/stack-release.sh:14`):
|
||||
+
|
||||
+```sh
|
||||
+sudo -u mosaic -H env PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
+ /srv/mosaic/src/mosaic-stack/scripts/stack-release.sh adopt <business> --instance dev --units external
|
||||
+```
|
||||
+
|
||||
@@ -211,0 +221,4 @@ Each upgrade, before `stack-upgrade.sh upgrade`:
|
||||
+Only the first upgrade after adopt takes `--no-rehearsal`: append it to the upgrade command below.
|
||||
+Adopt writes no backup, so omitting the flag then refuses `no-backup`. Subsequent upgrades omit it;
|
||||
+when a backup exists, supplying it refuses `rehearsal-required`.
|
||||
+
|
||||
@@ -246,0 +260 @@ step requiring Mos's go (`scripts/stack-copy.sh:3`); the install does not create
|
||||
+The step runs under sudo's `secure_path`, so it sets its own PATH and umask.
|
||||
@@ -250,0 +265,2 @@ cd / || exit 1
|
||||
+umask 077
|
||||
+PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
No matching test reads the changed lines, so the stop condition did not apply.
|
||||
No test run or slot is required under sage-3791. Verification is static: the
|
||||
required grep and positive control above, exact brief/base pins, scoped diff,
|
||||
and git diff --check. Reviewer Boyd and Sage own review/landing; no approval
|
||||
or landing is claimed here.
|
||||
|
||||
Receipt correction: the first commit embedded ordinary diff context lines that
|
||||
contain a single space. git diff --check flagged those as trailing whitespace.
|
||||
This follow-up uses the exact zero-context diff instead. The run sheet is
|
||||
unchanged, no tests or Node commands run, and no amend or force-push is used.
|
||||
|
||||
## F11 addition (sage-3805, mos-1549, ops-0275)
|
||||
|
||||
The brief was extended after F7/F10 drafting and re-pinned at revision3733:
|
||||
canonical local commit a9bae9af2, same F7 section anchor, full verified blob
|
||||
0e684ef2103447dcc2013b6bf81b1c4ca5bbc0bd (requested pin0e684ef21034).
|
||||
Read using git show from the canonical checkout; no fetch, rebase or queue write
|
||||
is required. This new pin supersedes the earlier brief pin for this follow-up.
|
||||
|
||||
Defect: a precheck one second after unit start found no bus.sqlite yet.
|
||||
Section10 now requires loopback healthz success after a unit start before any
|
||||
case2-precheck.sh, including section9's --library-status. It uses the same
|
||||
127.0.0.1:<port>/healthz curl form as scripts/case1-accept.sh:458. The subshell
|
||||
tries at most30 times, each curl capped at2 seconds, with1 second between failed
|
||||
attempts (at most89 seconds plus scheduling overhead). Exhaustion prints STOP
|
||||
and exits only the subshell. The instruction says to stop before precheck/adopt;
|
||||
it does not exit the interactive operator shell. The section9 library-gitea row
|
||||
now explicitly requires this bounded wait before --library-status. <port> is the
|
||||
installed Console unit's --port, not an assumed port or business name.
|
||||
No host action, test, Node command or credential access occurred.
|
||||
|
||||
Required grep repeated for the widened scope:
|
||||
|
||||
```sh
|
||||
git grep -n 'host-run-sheet' -- 'packages/*/tests/*' 'scripts/test-*.sh'
|
||||
# exit1, no matches
|
||||
git grep -n 'case2-precheck.sh' -- 'packages/*/tests/*' 'scripts/test-*.sh'
|
||||
# exit0, positive control; output follows
|
||||
packages/cli/tests/case1-accept.test.mjs:26:const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
|
||||
packages/cli/tests/case2-install.test.mjs:2:// scripts/case2-precheck.sh's library checks and --library-status mode. Brief:
|
||||
packages/cli/tests/case2-install.test.mjs:23:const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
packages/cli/tests/case2-install.test.mjs:271:// ---- case2-precheck.sh, the library profile (D6a) ----
|
||||
packages/cli/tests/case2-install.test.mjs:334:// ---- case2-precheck.sh --library-status (D6b) ----
|
||||
packages/cli/tests/case2-unit.test.mjs:2:// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
|
||||
packages/cli/tests/case2-unit.test.mjs:20:const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
packages/cli/tests/case2-unit.test.mjs:588:// ---- case2-precheck.sh ----
|
||||
```
|
||||
|
||||
No test reads the changed run-sheet lines; the stop condition does not apply.
|
||||
No test run is needed under the docs-only brief. Static verification checks both
|
||||
locations, retry limit/timeout, the loopback URL, unchanged F7/F10 and scoped diff.
|
||||
|
||||
F11 delta against d9400f0d3 (git diff --unified=0):
|
||||
|
||||
```diff
|
||||
diff --git a/docs/guides/host-run-sheet.md b/docs/guides/host-run-sheet.md
|
||||
index 96fd4f363..e9fa87f24 100644
|
||||
--- a/docs/guides/host-run-sheet.md
|
||||
+++ b/docs/guides/host-run-sheet.md
|
||||
@@ -322 +322 @@ file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's bus
|
||||
-| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | `case2-precheck.sh --business <business> --library-status`, after the bus is up: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
+| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | After a unit start, wait for loopback `/healthz` with the bounded retry in section 10 before `case2-precheck.sh --business <business> --library-status`: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
@@ -357,0 +358,16 @@ None by hand. I2's `case2-unit.sh` enables both units, starts the bus, then the
|
||||
+After a unit start, wait for the Console's loopback `/healthz` to succeed before running
|
||||
+`case2-precheck.sh`, including `--library-status` in section 9. Replace `<port>` with the installed
|
||||
+Console unit's `--port`. This retry is bounded to 30 attempts, each with a 2-second timeout, with
|
||||
+1 second between failed attempts. If it prints STOP, stop here; do not run the precheck or adopt.
|
||||
+
|
||||
+```sh
|
||||
+(
|
||||
+ attempts=0
|
||||
+ until curl -fsS --max-time 2 -o /dev/null 'http://127.0.0.1:<port>/healthz'; do
|
||||
+ attempts=$((attempts + 1))
|
||||
+ [ "$attempts" -lt 30 ] || { echo "STOP: loopback healthz did not become ready; do not continue" >&2; exit 1; }
|
||||
+ sleep 1
|
||||
+ done
|
||||
+)
|
||||
+```
|
||||
+
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
# Goldie
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-1542 and mos-1546. Owns F8 and F9, the library persona
|
||||
import in `scripts/case2-install.sh` (#1818), for the 433 Run B rerun.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `goldie/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and review the install path (mos-1542 rule 4). Registered by
|
||||
Sage on 2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1,331 +0,0 @@
|
||||
# Row 226 build notes, C2-13b install script and library precheck (#1727)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck" (:1073-1134 at
|
||||
d37210288). Design: agents/gosalyn/work/case2/DESIGN.md r11 sections 4 and 10, on gosalyn/case2-design
|
||||
75fb3e10e. Row 195's needs: agents/darkwing/work/queue-195/DESIGN.md "What row 195 needs from row 226".
|
||||
|
||||
This is design round d2. Nothing is built yet. d1 was f46e66883, and Webby's review of it is webby-0085
|
||||
(CHANGES, B1 and N1-N6) and webby-0086 (the ordering problem). Sage ruled on both in sage-2189. The
|
||||
changes from d1 are listed at the end.
|
||||
|
||||
## Base
|
||||
|
||||
Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are done.
|
||||
|
||||
## Facts the design rests on
|
||||
|
||||
- No CLI writes a business file. `mosaic business validate <id>` checks one and exits 0 ok, 2 invalid
|
||||
(packages/business/src/cli.mjs). A business file needs `human`, `arbiters`, `projects`, `tracker`
|
||||
with a sync bot and a Vikunja credential ref, and `roles`, each role with its tracker bot when its
|
||||
definition uses Vikunja (business.mjs:23, :84-100, :131-149).
|
||||
- The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping`
|
||||
(packages/library/src/mapping.mjs). There is no top-level `library` key.
|
||||
- `library.sync` and `library.status` are bus verbs, human only (broker.mjs:92, :2452-2476). The only
|
||||
client is `packages/bus/src/human-cli.mjs` over the bus socket.
|
||||
- The bus makes each library mirror at start, `ensureMirror({ business, project, dataRoot })`
|
||||
(packages/bus/src/runtime.mjs:143-145). Before the bus first starts there is no mirror.
|
||||
- `sync()` fetches outside `withLibraryLock`. Only a per-process `running` set keeps two syncs apart
|
||||
(packages/library/src/sync.mjs:97-98), so a second process calling `sync()` would race the bus's
|
||||
timer.
|
||||
- `libraryStatus(context)` reads the recorded state under the flock and writes nothing
|
||||
(mirror.mjs:232-239). A missing or unsafe mirror fails `library-git` (`ownedRoot`, mirror.mjs:43-49).
|
||||
A mirror that has never synced returns the empty state, `lastFetch: null`, `lastError: null`,
|
||||
`problems: []` (mirror.mjs:27, :205-210). `lastError` is `{ code, at }` with `code` matching
|
||||
`^[a-z-]{1,64}$` (mirror.mjs:225).
|
||||
- The bus builds the library context as `{ business, project, dataRoot }` and the slot as
|
||||
`<slotDir>/secrets/<business.id>/library-gitea` (runtime.mjs:146-151, :162-164).
|
||||
- `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes
|
||||
`<configDir>/personas/<business>/<instance>.md` at 0600 in 0700 dirs, never overwriting, and prints
|
||||
`instance bean: persona.name Bean, persona.sha256 <hex>` per persona.
|
||||
- A Discord binding is `<dataRoot>/discord/<name>.json` (packages/discord/src/binding.mjs:70-83).
|
||||
- `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which
|
||||
`stack-copy.sh copy COMMIT --copies DIR` makes. The copy runs `npm ci` from package-lock.json
|
||||
(stack-copy.sh:70), which installs the pinned pi.
|
||||
|
||||
## Design
|
||||
|
||||
### D1. The business file is an input
|
||||
|
||||
Unchanged from d1. `case2-install.sh` takes `--business-file FILE`, a JSON business document written for
|
||||
the host from Jason's go. The script doesn't build one from flags, because bots, botIds and credential
|
||||
refs are host values and the brief says every host value is an input. It checks the input, adds what
|
||||
only it knows, writes it, and then runs `mosaic business validate`, so business.mjs stays the only
|
||||
schema.
|
||||
|
||||
What the script checks before it writes anything, beyond `validate`:
|
||||
- `id` equals `--business`.
|
||||
- `library`: at least one library project, `bus.tester` not true, roles `bean` and `chet` with
|
||||
definition `companion`, and no `persona.*` var in the input, since D3 adds them.
|
||||
- `factory`: no library project, and `bus.tester` true. These are the precheck's rules at
|
||||
case2-precheck.sh:127-135, so the install refuses what the precheck would refuse.
|
||||
|
||||
### D2. Install steps, in order
|
||||
|
||||
1. Refuse to run as root.
|
||||
2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config.
|
||||
3. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
|
||||
`case2-unit.sh --copy` names has to exist before root renders the units (row 195 need 3).
|
||||
4. `library` only: the persona import's `install` step from `--personas DIR` (D3).
|
||||
5. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4).
|
||||
6. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, design 10
|
||||
step 4.
|
||||
7. `mosaic business validate <id>`. If it refuses and step 5 created the file in this run, remove that
|
||||
file, then refuse. A file that already matched on a re-run (D4) is never removed (N4).
|
||||
8. Print the copy id and the next steps: root's 2c, Jason's 2d, 3 and 4, then the `--library-status`
|
||||
check (D6b).
|
||||
|
||||
d1's step 3, `release.sh package` and `activate`, is gone (Q4a, webby-0085 B1, sage-2189). Package is a
|
||||
`docker compose build`, and activate runs a pi Compose health check that mounts `PI_AUTH_FILE`
|
||||
(compose.yaml:43). Neither fits 2a, which has no Docker and no pi credential, and the hub doesn't run
|
||||
containers. The pinned harness comes from the copy's `npm ci` (Q3). The `CASE2_RELEASE` seam goes with
|
||||
it.
|
||||
|
||||
It starts no unit, writes no unit file, doesn't write `/etc/mosaic-stack/dev.args` and doesn't run
|
||||
`adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged. Row 195's W8 now
|
||||
passes `--business-file` and has no release step.
|
||||
|
||||
### D3. Personas come from the staging dir
|
||||
|
||||
Unchanged from d1. `library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs
|
||||
stage`. The install runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir>
|
||||
--business <id>` and reads its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only
|
||||
those two lines go into the business file. They hold a name and a digest, never text.
|
||||
|
||||
This moves the persona import's stage part of design 10 step 6 ahead of 2b: the read of her Hermes
|
||||
profiles and Jason's check of the printed pairing. Webby agreed in webby-0085 (Q1).
|
||||
|
||||
### D4. Re-runs
|
||||
|
||||
Unchanged from d1, plus N4. Each step is safe to run again. bootstrap and the copy are idempotent. An
|
||||
existing business file that matches the one the script would write passes, and the script remembers it
|
||||
didn't create it. One that differs refuses `business-file-differs`, and the script doesn't touch it.
|
||||
The persona import already refuses to overwrite. On a re-run the install compares each existing
|
||||
persona file's sha256 with the staging manifest, and refuses on a mismatch.
|
||||
|
||||
### D5. Inputs and seams
|
||||
|
||||
```
|
||||
case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library]
|
||||
[--personas DIR] [--home DIR]
|
||||
```
|
||||
|
||||
`--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is
|
||||
honoured as bootstrap.sh honours it. One test seam, in case2-unit.sh's `CASE2_*` pattern:
|
||||
`CASE2_STACK_COPY` is the copy script, and the test uses a stub that makes the copy dir.
|
||||
|
||||
Exit codes: 0 ok, 1 refused, 4 usage, as case2-unit.sh uses.
|
||||
|
||||
### D6. Precheck library checks, split in two (Q4b, sage-2189)
|
||||
|
||||
d1 ran `sync()` from the precheck. Webby's N1 and webby-0086 showed two faults. The precheck runs before
|
||||
the units start, so there is no mirror yet (runtime.mjs:143-145). And a precheck `sync()` after the bus
|
||||
is up would race the bus's timer, since `sync()` fetches outside the lock (sync.mjs:97-98). Sage split
|
||||
the check in sage-2189, and the brief's "runs library.sync" becomes "reads the bus's recorded sync
|
||||
state".
|
||||
|
||||
#### D6a. Before the units start, `--profile library`
|
||||
|
||||
These run where the precheck runs today, after 2c's render and before the start. They sync nothing and
|
||||
read no mirror. Each is a `refuse` line in the script's style.
|
||||
- The business file is owned by the uid, with no group or other bit (`business-file-mode`).
|
||||
`businesses/.history/<id>/` is 0700 and its `changes.jsonl` is 0600, when present (`history-mode`).
|
||||
- The persona dirs are 0700. Each creative role's persona file is regular, owned and 0600
|
||||
(`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output
|
||||
prints names and digests, never text.
|
||||
- No `<dataRoot>/discord/*.json` exists (`discord-binding`).
|
||||
|
||||
#### D6b. `case2-precheck.sh --business ID --library-status`, after the bus is up and after step 4
|
||||
|
||||
A new mode, run as `<user>` once the bus is up and Jason has placed the token. It changes nothing. A
|
||||
node block builds the context the bus builds, `{ business, project, dataRoot }` for each library
|
||||
project, with `dataRoot` from the config (N3, runtime.mjs:146-151, :162-164). For each one it:
|
||||
- refuses `library-mirror-missing` when the mirror path doesn't exist. It checks this itself, so the
|
||||
message says the bus hasn't made it, not `library-git`;
|
||||
- calls `libraryStatus(context)` only, and refuses with the thrown code when that fails, for example
|
||||
`library-git` for an unsafe mirror or `library-sync-busy` when the flock wait runs out;
|
||||
- refuses `library-never-synced` when `lastFetch` is null;
|
||||
- refuses `library-last-error <code>` when `lastError` is not null. The code is the fixed
|
||||
`^[a-z-]{1,64}$` string, so it's safe to print. This also covers Webby's N2;
|
||||
- prints each recorded R-problem as `problem R<n> <path>`, with the path through `JSON.stringify`, then
|
||||
one `problems <count>` line (N5). Her file names are the only part that prints, never text.
|
||||
R-problems don't refuse (Q2, agreed by Webby).
|
||||
|
||||
The bus syncs each library with a `user` every 60 s, so the first run after step 4 may need a minute,
|
||||
or Jason's own `library.sync`. The run sheet says so.
|
||||
|
||||
### D7. host-run-sheet.md fix
|
||||
|
||||
`docs/guides/host-run-sheet.md:240` says "I2's install writes it" about `dev.args`. It becomes: root
|
||||
writes it at the unit render, from the values the go names, and the install never writes it, because
|
||||
`<user>` can't write `/etc` and root reads nothing `<user>` wrote. Sage asked in sage-1832 for this fix
|
||||
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:275) says
|
||||
"C2-13b's `library.sync` (not landed)". It becomes the `--library-status` check (D6b). The install
|
||||
section also gets a line that the copy's `npm ci` needs the npm registry from the host (Q3, webby-0085).
|
||||
|
||||
### D8. Design 10 text
|
||||
|
||||
Sage ruled that design 10 gets the same change when 226 lands, with the line in this row's manifest.
|
||||
In agents/gosalyn/work/case2/DESIGN.md:
|
||||
- step 2b drops `release.sh package` and `activate`;
|
||||
- a new step after step 4 runs `case2-precheck.sh --business <business> --library-status`;
|
||||
- the precheck's library text says it reads the bus's recorded sync state, not that it runs
|
||||
`library.sync`.
|
||||
|
||||
### D9. Tests, `packages/cli/tests/case2-install.test.mjs`
|
||||
|
||||
These follow case2-unit.test.mjs: scratch HOME, MOSAIC_CONFIG, stub bin dir and seams, a local bare repo
|
||||
cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output.
|
||||
- Install `library`:
|
||||
- writes the business file at 0600 with bean's and chet's persona vars;
|
||||
- writes the persona files at 0600 in 0700 dirs and the empty slot dir at 0700;
|
||||
- logs the copy call and no release call;
|
||||
- starts nothing;
|
||||
- passes on a re-run;
|
||||
- refuses `business-file-differs` and leaves the file unchanged.
|
||||
- Install `factory`:
|
||||
- writes no library mapping, no persona file and no slot dir, which is the brief's case;
|
||||
- refuses a library project and `bus.tester` off.
|
||||
- Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a
|
||||
`validate` refusal, after which no business file is left.
|
||||
- N4: a re-run where the matching business file is already there and `validate` refuses leaves that
|
||||
file in place, byte for byte.
|
||||
- D6a refusals, one case each: a persona file at 0644, a digest mismatch, a business file at 0644, a
|
||||
`changes.jsonl` at 0644, and a Discord binding. A clean library host passes D6a with no mirror.
|
||||
- D6b, with the mirror made by `ensureMirror` and the state written by a real `sync()` against the local
|
||||
bare repo, as the bus would:
|
||||
- no mirror refuses `library-mirror-missing`;
|
||||
- a fresh mirror refuses `library-never-synced`;
|
||||
- a sync against a remote that doesn't resolve refuses `library-last-error` with its code;
|
||||
- a synced bare repo with one R2 path prints its `problem` line and `problems 1`, and passes.
|
||||
|
||||
Every new refusal gets a mutant that removes it. The count goes in the slot request.
|
||||
|
||||
### D10. Files
|
||||
|
||||
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks and
|
||||
`--library-status` only) and `packages/cli/tests/case2-install.test.mjs` (new). Also
|
||||
`docs/guides/host-run-sheet.md` (D7, sage-1832), agents/gosalyn/work/case2/DESIGN.md (D8, sage-2189)
|
||||
and this BUILD.md. Before the slot request I'll grep for outside readers of case2-precheck.sh, since
|
||||
case2-unit.test.mjs runs it.
|
||||
|
||||
## Questions
|
||||
|
||||
All three d1 questions are settled:
|
||||
- Q1, persona stage before 2b: Webby agreed (webby-0085).
|
||||
- Q2, R-problems don't refuse: Webby agreed.
|
||||
- Q3, the pinned harness is the copy's `npm ci`: Webby agreed, and asked for the registry note in D7.
|
||||
- Q4a and Q4b: Sage, sage-2189.
|
||||
|
||||
One choice for Webby to check: D6b's `problem` line carries the path through `JSON.stringify`. N5 asked
|
||||
for a prefix and a count. I kept the path because Jason needs it to tell her which file to fix, and the
|
||||
names are hers, not text.
|
||||
|
||||
## Changes from d1
|
||||
|
||||
- B1: release package and activate dropped, with the `CASE2_RELEASE` seam and its test (D2, D5).
|
||||
- N1 and webby-0086: the sync is gone from the precheck. D6 splits into D6a before the start and D6b,
|
||||
`--library-status`, after the bus is up and step 4 (sage-2189).
|
||||
- N2: covered by D6b's `lastError` refusal.
|
||||
- N3: D6b builds the context and slot as the bus does.
|
||||
- N4: step 7 removes only a file this run created, with a test.
|
||||
- N5: `problem` prefix and a `problems` count.
|
||||
- N6: the run sheet lines are :240 and :275.
|
||||
- New D8, the design 10 text change, and its path in D10.
|
||||
|
||||
## Build
|
||||
|
||||
Webby approved d2 in webby-0087 (REVIEW-design-d2.md, webby/reviews 2f488b4ea). Nothing above this
|
||||
section changed after approval. This section records the build.
|
||||
|
||||
### D8 lands on case2-design
|
||||
|
||||
Sage ruled in sage-2195, after Webby's N1, that D8 goes on gosalyn/case2-design, because
|
||||
agents/gosalyn/work/case2/DESIGN.md isn't on refactor. The change is commit 624212842 there, pushed:
|
||||
step 2b drops `release.sh package` and `activate`, new step 4a runs `--library-status`, and the C2-13b
|
||||
row text says the precheck reads the bus's recorded sync state. DESIGN.md is not in this row's manifest.
|
||||
|
||||
The brief's lines docs/plans/2026-10-20_case2-rows.md:1093-1096 still name release package, activate and
|
||||
a library.sync fetch. Sage owns the brief and will amend those lines to match D8 in a doc-only commit
|
||||
after the re-pin window (sage-2205). They are not in this row's manifest.
|
||||
|
||||
### Webby's d2 notes
|
||||
|
||||
- N2: the `problem` line keeps the path. After `JSON.stringify` the precheck also escapes DEL, the C1
|
||||
controls U+0080-009F, U+200E and U+200F, U+2028 and U+2029, and the bidi controls U+202A-202E and
|
||||
U+2066-2069 as `\uXXXX`. Webby named C1 and bidi. I added the rest because `JSON.stringify` leaves
|
||||
them raw too, and each one can change how a terminal shows the line. The test path holds U+202E and
|
||||
U+0085, and the test checks that neither prints raw.
|
||||
- N3: `lastError` is checked before `lastFetch`. A first sync with no token records
|
||||
`library-credential-missing` and leaves `lastFetch` null, so it refuses `library-last-error`, not
|
||||
`library-never-synced`. The fresh-mirror test has `lastError` null and refuses `library-never-synced`.
|
||||
|
||||
### Choices made in the build
|
||||
|
||||
- The config dir. The persona import refuses a config dir with group or other bits, and bootstrap makes
|
||||
one with the default mode. So the install makes the config dir at 0700 when it's absent, before
|
||||
bootstrap, and refuses `config-dir-mode` when it exists with a group or other bit. It checks
|
||||
`secrets/` and `secrets/<id>/` the same way under library (`secrets-mode`, `slot-mode`).
|
||||
- `--home` must equal `$HOME` (`home-mismatch`). Every path comes from `$HOME` and `MOSAIC_CONFIG`, as
|
||||
bootstrap's does, so a different `--home` would only name a second home the script doesn't use.
|
||||
- A plan step runs before anything is written. It reads the input and the staging manifest, computes
|
||||
the persona vars from the manifest, and compares the result with what's on disk. It refuses every
|
||||
input rule, `business-file-differs` and `persona-differs` there, so a refused run writes nothing.
|
||||
After the import the script checks that the import's two `instance` lines carry the manifest's
|
||||
digests.
|
||||
- A re-run with both persona files present and matching skips the import, which would refuse to
|
||||
overwrite them. One matching file and one missing refuses `persona-differs`.
|
||||
- A diverged library prints one line under `--library-status`, naming the tip `library.sync --accept`
|
||||
would take. It doesn't refuse. The design doesn't list it as a refusal, and the bus still serves the
|
||||
old tip.
|
||||
- The precheck's library checks print `ok` lines for each check they pass, as the rest of the script
|
||||
does. A business file that's missing prints no extra line, since the existing business-file check
|
||||
already refuses it.
|
||||
- The business id must match the persona import's rule, `^[a-z0-9][a-z0-9-]{0,63}$`. That's narrower
|
||||
than the precheck's, which also takes `.` and `_`, because the persona dir uses the id.
|
||||
|
||||
### Outside readers
|
||||
|
||||
`git grep` over packages/*/tests/*, packages/*/src/* and scripts/test-*.sh finds case2-precheck.sh read
|
||||
by case2-unit.test.mjs, which runs it, and case1-accept.test.mjs, which names a copy's path but runs a
|
||||
fake. No test reads host-run-sheet.md. No test pins a hash of either script. case2-unit.test.mjs's
|
||||
precheck host has a 0600 business file and no persona vars, so the new D6a checks pass there.
|
||||
|
||||
### Mutants
|
||||
|
||||
24, in /mnt/storage/scratch/gosalyn-r226/mutate.py. Each replaces one exact string once. Line numbers
|
||||
are the candidate's. run.sh stops with BASE RED before any mutant unless the base run is rc=0 and
|
||||
fail 0.
|
||||
|
||||
| Mutant | Line | Check removed |
|
||||
|---|---|---|
|
||||
| I1 | install :73 | run-as-root |
|
||||
| I2 | install :74 | home-mismatch |
|
||||
| I3 | install :81 | config-dir-mode |
|
||||
| I4 | install :101 | business-id-mismatch |
|
||||
| I5 | install :107 | profile-library-business |
|
||||
| I6 | install :108 | bus-tester-off |
|
||||
| I7 | install :111 | bus-tester-on |
|
||||
| I8 | install :115 | persona-var-input |
|
||||
| I9 | install :158 | business-file-differs |
|
||||
| I10 | install :152 | persona-differs |
|
||||
| I11 | install :218 | N4, removes the file even when this run didn't write it |
|
||||
| I12 | install :218 | validate's removal of the file this run wrote |
|
||||
| P1 | precheck :229 | business-file-mode |
|
||||
| P2 | precheck :235 | history-mode for changes.jsonl |
|
||||
| P3 | precheck :246 | persona-mode for a persona file |
|
||||
| P4 | precheck :251 | persona-mismatch |
|
||||
| P5 | precheck :241 | persona-mode for a persona dir |
|
||||
| P6 | precheck :258 | discord-binding |
|
||||
| P7 | precheck :104 | library-mirror-missing |
|
||||
| P8 | precheck :107 | library-last-error |
|
||||
| P9 | precheck :107 | N3, the order swapped |
|
||||
| P10 | precheck :108 | library-never-synced |
|
||||
| P11 | precheck :92 | N2, the escape after stringify |
|
||||
| P12 | precheck :65 | --library-status refuses --profile |
|
||||
|
||||
`history-mode` for the history dir itself has no mutant and no test. The brief names only the
|
||||
`changes.jsonl` case.
|
||||
|
||||
### Tests
|
||||
|
||||
Not run yet. They wait for a slot.
|
||||
@@ -1,177 +0,0 @@
|
||||
# Row 239 (#1745) build notes (append only)
|
||||
|
||||
## 2026-10-22 b1 (Honker)
|
||||
|
||||
Design d1 5da5f28f6 was approved by Duckworth with C1 and C2 and no blockers (duckworth-0165, note
|
||||
agents/duckworth/work/row239/REVIEW-design-d1.md). The code is on honker/row239 from aca16404c. No test
|
||||
has run yet: every run waits for Sage's CLEAR.
|
||||
|
||||
### Duckworth's conditions and answers
|
||||
|
||||
- C1 (option a): `outcome-unknown-revoked` is in the Console's `OPEN` list (webui bus.mjs), so
|
||||
`sendOutcome` answers it as `outcome-unknown` with `sent: 'unknown'` and never "Nothing changed". T6
|
||||
covers it and M3f is its mutant.
|
||||
- C2: `write()` sets `revoked = true` after `beforeWrite()` returns, and a catch around the whole
|
||||
`withLock(...)` call marks the error. So a throw from the lock's release in its finally is marked as
|
||||
well. This is structural and has no test, as Duckworth said.
|
||||
- Q1: `beforeWrite` runs after both validations. Q2: two codes. Q3: M2 removes the `:188` check (now
|
||||
organization.mjs:195).
|
||||
|
||||
### Notes N1-N5
|
||||
|
||||
- N1: `beforeWrite` must not write the same business, because it would find its own lock and refuse
|
||||
`organization-busy`. This is said in the comments on `write()` and `commit()`. Today's revokes (the
|
||||
bus's credential revoke) write no business file.
|
||||
- N2: the lock is held across the revoke. A fire or a delete now keeps other writers of that business
|
||||
out for as long as the revoke takes. They refuse `organization-busy`, as they do for any held lock.
|
||||
- N3: a revoke that fails partway (some tokens revoked, then `credential-revoke-failed`) behaves as
|
||||
today. The error carries no `revoked` mark, because `beforeWrite` didn't return, and nothing is
|
||||
written. This row doesn't change that.
|
||||
- N4: a delete closed with `outcome-unknown-revoked` exits 3 in the CLI, as every closed refusal does.
|
||||
Exit 1 (transport EXIT) applies only to a direct verb answer.
|
||||
- N5: I grepped for "before the write" and similar comments. The ones left (lifecycle-revoke.test:2,
|
||||
instance-settings:56, cli instances.mjs:53, serve.mjs:365) are still true and are unchanged. T21 in
|
||||
bus lifecycle-revoke.test:173 is unchanged.
|
||||
|
||||
### Differences from the design
|
||||
|
||||
- The business tests are in lifecycle-settings.test.mjs, not instance-settings.test.mjs. The fire tests
|
||||
and their fixture already live there. The fixture now returns `dir` and `rolesDir` too.
|
||||
- T3 covers T3b (the delete) in the same test, looping over fire and delete. It also checks that a
|
||||
revoke throwing `credential-revoke-failed` carries no mark and writes nothing.
|
||||
- T4b is the plain-code check. The design's "T4b, outcome-unknown-revoked for an fs-style error" is in
|
||||
T4, which checks EIO with and without the mark for both a fire and a delete.
|
||||
- The bus test's fake org gets `deleteError`, thrown from its `instances.set` branch, so T4 can drive a
|
||||
delete.
|
||||
- transport.test gets one assertion, `busExit("outcome-unknown-revoked") === 1`, for M3g.
|
||||
- A latent Console text: a direct (not closed-request) `revision-conflict-revoked` answer in the
|
||||
Console would still get " Nothing changed." appended, because only `OPEN` codes skip it. That sentence
|
||||
is true there: the file is unchanged. No direct human path gives this code today, since a delete
|
||||
reaches the Console as a closed request's refusal.
|
||||
- The Console's STATUS entry for `outcome-unknown-revoked` (503) can't be reached while the code is in
|
||||
`OPEN`. It stays for symmetry with the CLI and is harmless.
|
||||
|
||||
### What changed
|
||||
|
||||
- packages/business/src/organization.mjs: `write()` takes `beforeWrite`, runs it under the lock after
|
||||
the revision check and both validations, and marks any later error `revoked`. The body is reindented
|
||||
inside the try.
|
||||
- packages/business/src/instance-settings.mjs: `instanceSettings` takes an optional `writer`. `commit()`
|
||||
passes `beforeWrite` to `writer.write` instead of calling it. The early check stays, and its comment
|
||||
is rewritten.
|
||||
- packages/bus/src/broker.mjs: `#organizationFail` maps a marked error to `revision-conflict-revoked` or
|
||||
`outcome-unknown-revoked` before the existing mappings. `#fireJob`'s notice adds "Its credential
|
||||
tokens may already be revoked." for a `-revoked` code.
|
||||
- packages/bus/src/runtime.mjs: comment only.
|
||||
- packages/cli/src/instances.mjs: two REASONS entries. packages/cli/src/transport.mjs: EXIT
|
||||
`outcome-unknown-revoked` 1.
|
||||
- packages/webui/src/serve.mjs: two AGENT_REASONS entries. packages/webui/src/bus.mjs: `OPEN` entry and
|
||||
two STATUS entries.
|
||||
- Tests:
|
||||
- business lifecycle-settings.test: "row 239 T1", "row 239 T2", "row 239 T3".
|
||||
- bus fire-revokes-grants.test: "row 239 T4", "row 239 T4b".
|
||||
- cli instances.test: "row 239 T5 CLI". cli transport.test: one assertion in "busExit and
|
||||
refuseInsideAgent".
|
||||
- webui agents.test: "row 239 T6".
|
||||
- bus channel-proof.test re-pins packages/cli/src/transport.mjs (662642a8...) and
|
||||
packages/webui/src/bus.mjs (1eba9774...), with a comment on each. Neither changes a proof or the
|
||||
transport. The design's file list missed this. The pin grep found it before the first run.
|
||||
### Mutants for the gate (exact-once replacements)
|
||||
|
||||
- M1 (instance-settings.mjs, beforeWrite before the lock): replace
|
||||
` const receipt = writer.write(business, next, { revision, actor, context, beforeWrite });` with
|
||||
` if (beforeWrite) beforeWrite();\n const receipt = writer.write(business, next, { revision, actor, context });`.
|
||||
The kill is lifecycle-settings "row 239 T1" (the revoke spy has a call).
|
||||
- M2 (organization.mjs, the fire refusal dropped): delete the line
|
||||
` if (revision !== before) fail("revision-conflict", "organization revision changed; read again");`.
|
||||
The kill is "row 239 T1" (the write lands over the hand edit).
|
||||
- M3a (organization.mjs, the mark dropped): ` if (revoked && e instanceof Error) e.revoked = true;`
|
||||
becomes ` void revoked;`. The kills are "row 239 T2" and "row 239 T3" (business). T4 drives the mark
|
||||
through a fake org, so it doesn't see M3a.
|
||||
- M3b (broker.mjs): delete the line
|
||||
` if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");`.
|
||||
The kill is fire-revokes-grants "row 239 T4".
|
||||
- M3c (cli instances.mjs): delete the five lines from ` "revision-conflict-revoked": "the business file`
|
||||
through ` "business file again with \`mosaic business instances\` before trying again",`. The kill is
|
||||
instances.test "row 239 T5 CLI".
|
||||
- M3d (webui serve.mjs): delete the two lines starting ` 'revision-conflict-revoked': 'the business file`
|
||||
and ` 'outcome-unknown-revoked': 'the agent\'s tokens`. The kill is agents.test "row 239 T6".
|
||||
- M3e (broker.mjs): ` const tokens = code.endsWith('-revoked') ? ' Its credential tokens may already be revoked.' : '';`
|
||||
becomes ` const tokens = '';`. The kill is "row 239 T4".
|
||||
- M3f (webui bus.mjs, C1): the `OPEN` line loses `, 'outcome-unknown-revoked'`. The kill is "row 239 T6"
|
||||
(the launch POST answers 503 outcome-unknown, sent unknown).
|
||||
- M3g (cli transport.mjs): delete the line ` "outcome-unknown-revoked": 1,`. The kill is transport.test
|
||||
"busExit and refuseInsideAgent".
|
||||
|
||||
A kill counts only as a failed assertion in the named test, not a load error or a different test.
|
||||
|
||||
### Gate plan (mos-1275 timeouts)
|
||||
|
||||
- My row 256 gate (2026-10-22 10:34Z, same machine) took 590s for the cli package, 57s for webui and
|
||||
38s for test-foundation, the longest shell script.
|
||||
- Packages: `timeout -k 10 1800` per package (3x the cli run).
|
||||
- Each scripts/test-*.sh: `timeout -k 10 300` (more than 3x the 38s longest).
|
||||
- Single files and mutants: `timeout -k 10 600` per file run (cli instances.test whole is the longest
|
||||
single file).
|
||||
- Order on Sage's CLEAR: the touched test files whole (business lifecycle-settings, bus
|
||||
fire-revokes-grants and channel-proof, cli instances and transport, webui agents), then M1-M3g, then
|
||||
the full gate.
|
||||
- Before the in-review move: grep packages/*/tests for the changed comment and error text and for
|
||||
64-hex pins on the changed files, and drift-check BASE..refactor against them.
|
||||
|
||||
## 2026-10-22 r2 rebase (Honker)
|
||||
|
||||
- Duckworth approved b1 b0c28366f statically, with no blockers (duckworth-0168). On N1 (`endsWith('-revoked')`
|
||||
would also match `access-revoked`, which can't reach a fire job today), the check stays as it is, so the
|
||||
approved candidate doesn't change before the gate (honker-0163).
|
||||
- Rows 253 (48e450ce8) and 275 landed. b1 is rebased onto refactor 94c33a43c as honker/row239-r2. The
|
||||
push branch honker/row239 isn't rewritten.
|
||||
- Drift between aca16404c and 94c33a43c on the 15 paths: only packages/bus/src/broker.mjs, where row 275
|
||||
changes the viewer verbs. Neither of row 239's two hunks (`#fireJob` and `#organizationFail`) moves in
|
||||
meaning. The rebase applied with no conflict. The two pinned files (cli transport.mjs, webui bus.mjs)
|
||||
and channel-proof.test have no upstream change, so the b1 pins stand.
|
||||
- Each M1-M3g replacement string still matches exactly once.
|
||||
- The touched test files run whole again on the new base, in the slot (the rebase rule).
|
||||
|
||||
## 2026-10-23 r3 rebase (Honker, sage-3730)
|
||||
|
||||
- r2 (70c6e3772) is rebased onto refactor 93694c518 as honker/row239-r3. honker/row239-r2 isn't rewritten.
|
||||
- Between 94c33a43c and 93694c518, five of the 16 paths changed upstream: bus broker.mjs and runtime.mjs,
|
||||
channel-proof.test, and webui bus.mjs and serve.mjs. Every file merged on its own except channel-proof.test,
|
||||
where row 301 (59f0e44f4, mos-1449) re-pinned webui bus.mjs for the user transport fold. The merged
|
||||
pin keeps row 301's comment and row 239's, and takes sha256 e3f52172…1c2a89 of the merged bus.mjs.
|
||||
The transport.mjs pin (662642a8…6e55b) still matches the file.
|
||||
- Row 239's hunks in broker.mjs (`#fireJob`, `#organizationFail`), runtime.mjs (the revoke comment),
|
||||
bus.mjs (OPEN, STATUS) and serve.mjs are unchanged apart from their position. The range-diff shows
|
||||
only the channel-proof pin and the hunk context moved.
|
||||
- Each M1-M3g replacement string still matches exactly once. M3f's target is the whole `OPEN` line,
|
||||
since `, 'outcome-unknown-revoked'` also appears in the STATUS line.
|
||||
- No 64-hex pin in packages/*/tests or scripts names a changed file except channel-proof.test.
|
||||
- On the CLEAR (sage-3730): the touched files whole, then M1-M3g, then the counts here. Sage runs the
|
||||
full gate on the replay at landing.
|
||||
|
||||
## 2026-10-23 r4 replay and the slot run (Honker, sage-3773 CLEAR)
|
||||
|
||||
- On the CLEAR, refactor was 546366f4a (row 320 landed at 094124b4e). None of the 16 manifest paths changed
|
||||
between 93694c518 and 546366f4a. Upstream did change library src/index.mjs, which bus runtime.mjs imports,
|
||||
and business src/index.mjs, which the cli imports, so the run was on a replay onto 546366f4a rather than
|
||||
on r3's base. That replay is honker/row239-r4 0cae2dad5. The range-diff of r3 against r4 is four `=`
|
||||
lines with equal patch-ids, so duckworth-0292's read of r3 applies to r4. r3 isn't rewritten.
|
||||
- The run was on 0cae2dad5 with a clean tree, TMPDIR under /mnt/storage/scratch/tmp, 22:01:27Z-22:01:52Z.
|
||||
- The touched files, run whole: business lifecycle-settings 12/12, bus fire-revokes-grants 24/24, bus
|
||||
channel-proof 2/2, cli instances 15/15, cli transport 3/3, webui agents 9/9. That's 65/65 with 0 skipped,
|
||||
and no first-run red.
|
||||
- Mutants: 9 of 9 killed. Each was applied exactly once, and each made its named test fail as an assertion:
|
||||
- M1 lifecycle: 3 fail (T1, T2, T3).
|
||||
- M2: T1.
|
||||
- M3a: T2 and T3.
|
||||
- M3b fire-revokes-grants: T4.
|
||||
- M3c instances: T5 CLI.
|
||||
- M3d agents: T6.
|
||||
- M3e: T4.
|
||||
- M3f agents: T6.
|
||||
- M3g transport: "busExit and refuseInsideAgent".
|
||||
|
||||
T4b passed under M3b and M3e. Every restore matched the HEAD blob by sha256, and the tree was clean
|
||||
after the run.
|
||||
- No full gate (sage-3730): Sage runs it on the replay at landing.
|
||||
@@ -1,159 +0,0 @@
|
||||
# Row 239 design: the revoke under the writer's lock, and a fire test
|
||||
|
||||
Brief: docs/plans/2026-10-20_agent-lifecycle.md, "Brief: row 194 follow-up, the revoke under the writer's
|
||||
lock and a fire test" (blob 3b3b3b2b9), from Duckworth's row 194 b3 N1 and mos-1158. Base aca16404c (row
|
||||
194 landed as fd6dd327c, row 281 as e479521d3). Builder Honker, reviewer Duckworth, gate owner Sage.
|
||||
Draft 1, 2026-10-22.
|
||||
|
||||
## Today
|
||||
|
||||
- `instance-settings.mjs` `commit()` runs the checks, then compares the caller's revision with a fresh
|
||||
`writer.read` (row 194 b3, sage-1634), then calls `beforeWrite` (the fire's or the delete's credential
|
||||
revoke), then `writer.write`.
|
||||
- `writer.write` (`organization.mjs`) takes the lock, runs `recover`, checks the revision again (`:188`),
|
||||
validates, prepares, checks the bytes once more before publication (`:221`), renames and commits.
|
||||
- So two windows are left.
|
||||
- A write that lands between `commit()`'s read and the writer's lock still gets a revoke and then a
|
||||
`revision-conflict`.
|
||||
- Any refusal after the revoke ran (the `:221` check, or an I/O failure the bus reports as
|
||||
`outcome-unknown`) uses the same text as one before it, so nothing says the tokens may be gone.
|
||||
- Row 281 left this path alone. Its fire job (`broker.mjs` `#fireJob`) runs step G (grants), then step B,
|
||||
which is this same `write('instance.fire')`, and reports a step B failure as `fire.failed` along with a
|
||||
human notice that names the code. Its delete job only does step G. The delete itself still commits
|
||||
through `lifecycleDelete` → `instances.set` op delete → `commit()`.
|
||||
|
||||
## What changes
|
||||
|
||||
### 1. `beforeWrite` runs inside `writer.write`, under the lock (organization.mjs)
|
||||
|
||||
`write(id, document, { revision, actor, detail, context, beforeWrite = null })`. The order under the lock:
|
||||
|
||||
1. `recover`, read, then the `:188` revision check. A refusal here revokes nothing.
|
||||
2. Projects-protected and both validations. A refusal here revokes nothing either. I put `beforeWrite`
|
||||
after these rather than straight after `:188`, so a document the writer would refuse anyway never
|
||||
costs a revoke. Q1 asks Duckworth to confirm this.
|
||||
3. `beforeWrite()`. If it throws (today `credential-revoke-failed`), the error propagates as it is and
|
||||
nothing has been written, as now.
|
||||
4. Prepare, the `:221` check, rename and commit, wrapped so that any error thrown after step 3 gets
|
||||
`revoked = true` set on it before it is rethrown. That covers `revision-conflict` from `:221`, an
|
||||
`organization-refused` from `log()`, a barrier throw, and an fs error.
|
||||
|
||||
`record()` and the other writers (`launch-settings`, `plugin-settings`, `credentialVerbs`) pass no
|
||||
`beforeWrite` and behave as before.
|
||||
|
||||
### 2. commit() passes beforeWrite through (instance-settings.mjs)
|
||||
|
||||
`commit()` passes `beforeWrite` to `writer.write` and no longer calls it itself. The row 194 b3 early check
|
||||
stays. It costs one read, keeps T21's refusal before any validation work, and is no longer what protects
|
||||
the revoke. Its comment is rewritten to say so, and the "(BUILD.md follow-up)" sentence goes.
|
||||
|
||||
`instanceSettings({ ..., writer = organizationWriter({ dir, rolesDir }) })` takes an optional writer, so a
|
||||
test can inject one (the brief's "injected writer"). `runtime.mjs` passes none.
|
||||
|
||||
### 3. The bus names a refusal after a revoke (broker.mjs `#organizationFail`)
|
||||
|
||||
The bus answers with a code only, and the CLI and Console look the text up by code. So the "may already be
|
||||
revoked" signal has to be a code. Two new codes:
|
||||
|
||||
- `revision-conflict-revoked`: `e.revoked && e.code === 'revision-conflict'`.
|
||||
- `outcome-unknown-revoked`: `e.revoked` with any other code. The write may or may not have landed, since a
|
||||
failure after the rename is recovered as committed.
|
||||
|
||||
Both are checked before the existing mappings. An error without `revoked` maps exactly as today, so a
|
||||
refusal before any revoke keeps today's code and text. Q2 asks about the names.
|
||||
|
||||
### 4. The texts
|
||||
|
||||
- CLI `instances.mjs` REASONS:
|
||||
- `revision-conflict-revoked`: "the business file changed before the write, after the agent's tokens
|
||||
were revoked; the tokens may already be revoked and the instance can't use them; read the instances
|
||||
again and run the same command again, which is safe".
|
||||
- `outcome-unknown-revoked`: "the write's outcome is unknown and the agent's tokens may already be
|
||||
revoked; read the instances before trying again".
|
||||
- Console `serve.mjs` AGENT_REASONS: the same two, in the Console's wording ("delete again").
|
||||
- `#fireJob`'s `fire.failed` notice: when the code ends in `-revoked`, it adds "Its credential tokens may
|
||||
already be revoked." The chief's synchronous fire (no grants) answers with the code, as every chief verb
|
||||
does.
|
||||
- `transport.mjs` EXIT: `outcome-unknown-revoked` exits 1, as `outcome-unknown` does. Console
|
||||
`bus.mjs` STATUS: `revision-conflict-revoked` 409, `outcome-unknown-revoked` 503. A delete's code
|
||||
reaches both as a closed request's `refusal`, so these two only matter for a direct verb answer.
|
||||
|
||||
## Tests
|
||||
|
||||
- **T1, the brief's fire test** (business `instance-settings.test.mjs`). The fire uses an injected writer
|
||||
that wraps the real one. Its `write` makes one hand edit of the business file (a vars change on another
|
||||
instance) and then calls the real `write`, so the edit lands after `commit()`'s early check and before
|
||||
the lock's `:188` check. Expected:
|
||||
- the fire refuses `revision-conflict` without `revoked`;
|
||||
- the revoke spy has no calls;
|
||||
- the business file's bytes are the hand edit's;
|
||||
- changes.jsonl has no new prepared entry.
|
||||
- **T2** (same file). A fire on a real writer built with `barrier: stage => stage === 'prepared' && edit()`.
|
||||
The revoke runs, then `:221` refuses. Expected: `revision-conflict` with `revoked === true`, one revoke
|
||||
call, the file's bytes are the edit's, and the last receipt is `aborted`.
|
||||
- **T3** (same file). The same, with a `prepared` barrier that throws. Expected: the error has
|
||||
`revoked === true`, one revoke call, and the file unchanged. A delete through the same writer gives the
|
||||
same result (T3b).
|
||||
- **T4** (bus `fire-revokes-grants.test.mjs`, its injected `f.org`). `f.org.fireError` is a
|
||||
`revision-conflict` with `revoked: true`. Expected:
|
||||
- the job's `fire.failed` carries `revision-conflict-revoked`, and the inbox message says "may already
|
||||
be revoked";
|
||||
- the same with no `grants` (the synchronous fire) refuses `revision-conflict-revoked`;
|
||||
- a plain `revision-conflict` still maps to `revision-conflict` and the message lacks the sentence.
|
||||
- T4b: `outcome-unknown-revoked` for an fs-style error with `revoked`.
|
||||
- **T5** (cli `lifecycle.test.mjs` or `instances.test.mjs`, whichever already reads a closed delete's
|
||||
refusal). A delete closed with `revision-conflict-revoked` prints the new text, and one closed with
|
||||
`revision-conflict` prints today's.
|
||||
- **T6** (webui `agents.test.mjs`). The same pair for the Console's reason.
|
||||
- T21 (bus `lifecycle-revoke.test.mjs`) stays as it is, for delete.
|
||||
|
||||
## Mutants
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
| --- | --- | --- |
|
||||
| M1 | `beforeWrite` before the lock: `commit()` calls `beforeWrite()` itself and passes none to `write` | T1 (the revoke spy has a call) |
|
||||
| M2 | The fire refusal dropped: the `:188` revision check removed | T1 (the write lands over the hand edit) |
|
||||
| M3a | The mark dropped: no `revoked = true` in the catch | T2, T3, and T4 through the codes |
|
||||
| M3b | `#organizationFail` ignores `revoked` | T4 |
|
||||
| M3c | CLI text: the two REASONS entries removed | T5 |
|
||||
| M3d | Console text: the two AGENT_REASONS entries removed | T6 |
|
||||
| M3e | The notice: the `fire.failed` sentence removed | T4 |
|
||||
|
||||
M3a-M3e together are the brief's "drop the may already be revoked text".
|
||||
|
||||
## Files
|
||||
|
||||
- `packages/business/src/organization.mjs`
|
||||
- `packages/business/src/instance-settings.mjs`
|
||||
- `packages/business/tests/instance-settings.test.mjs`
|
||||
- `packages/bus/src/broker.mjs`
|
||||
- `packages/bus/tests/fire-revokes-grants.test.mjs`
|
||||
- `packages/cli/src/instances.mjs`
|
||||
- `packages/cli/src/transport.mjs`
|
||||
- one cli test file
|
||||
- `packages/webui/src/serve.mjs`
|
||||
- `packages/webui/src/bus.mjs`
|
||||
- `packages/webui/tests/agents.test.mjs`
|
||||
- This packet.
|
||||
|
||||
## Gate
|
||||
|
||||
As the brief says:
|
||||
|
||||
- Duckworth approves the manifest.
|
||||
- The bus, business, cli and webui suites and every `scripts/test-*.sh` are green on Sage's slot, with
|
||||
the touched test files run whole.
|
||||
- M1-M3e are killed.
|
||||
|
||||
Before the in-review move I grep the tests for the changed comment and error text, and for hash pins on the
|
||||
changed files.
|
||||
|
||||
## Questions for Duckworth
|
||||
|
||||
- **Q1.** Should `beforeWrite` run after the writer's validations (my pick) or straight after `:188` (the
|
||||
brief's literal place)? Running it later means a document the writer would refuse revokes nothing.
|
||||
- **Q2.** Are two new codes, `revision-conflict-revoked` and `outcome-unknown-revoked`, the right way to
|
||||
carry the signal? The alternative is to give `BusError` a detail field, which every CLI and Console
|
||||
reader would then have to learn. The codes are additive and no existing code changes meaning.
|
||||
- **Q3.** The brief's mutant "drop the fire refusal": I read it as removing the `:188` check (M2). If you
|
||||
meant the fire test's own refusal, say so and I'll redefine it.
|
||||
@@ -1,276 +0,0 @@
|
||||
# Row 283 (#1788) C2-01d build notes (Honker)
|
||||
|
||||
Append-only. Design: DESIGN.md d2 16e809457, APPROVE webby-0172. Gate owner Sage, reviewer Webby.
|
||||
|
||||
## 2026-10-23 build on 45dca69f6 (sage-3577)
|
||||
|
||||
Branch honker/row283-build from origin/refactor 45dca69f6e4bce52834587f66f2ff33ba839ab8a. The old branch
|
||||
honker/row283 (16e809457) held only the design, so its two commits were cherry-picked as 398314cf4
|
||||
(d1) and 4f66dccde (d2). The DESIGN.md blob equals 16e809457's.
|
||||
|
||||
Commits:
|
||||
- 331653b38: code, README and tests T1-T8.
|
||||
- 28918824c: the survey.test:73 re-pin, alone (mos-1330 item 4).
|
||||
- this file.
|
||||
|
||||
Nothing has run. No node, test or mutant ran during the build: Darkwing held the slot. Every count
|
||||
below is a plan until Sage's CLEAR.
|
||||
|
||||
### Drift check (sage-3577)
|
||||
|
||||
`git diff --stat 94c33a43c 45dca69f6` (design base to build base) over the paths the tests read or
|
||||
hash-pin:
|
||||
- No change in packages/library, packages/business/src/business.mjs or
|
||||
packages/business/tests/business.test.mjs.
|
||||
- No test hash-pins a library file. bus library.test and library-sync.test import index.mjs, and
|
||||
stack-copy.test only checks that index.mjs exists.
|
||||
- webui library-cli.test changed one line and doesn't read scope. The `classify` in webui reads.mjs is
|
||||
the business one, not the library's.
|
||||
|
||||
I redo the drift check on BASE..refactor before the in-review move.
|
||||
|
||||
### N3 (webby-0169): deepEquals of the mapping output outside packages/library
|
||||
|
||||
`git grep -n playground -- 'packages/*/tests/*' ':!packages/library/*'` and the files naming
|
||||
`universes` give two files:
|
||||
- business.test.mjs:20 compares `validateBusinessDocument(...).projects.books` with
|
||||
`checkMapping(mappingEntry())`. That is output against output, so `supporting: []` shows up on both
|
||||
sides. :27 compares only `.playground`.
|
||||
- library-projects-protected.test.mjs compares file bytes (:31, :32) and change paths (:58), never the
|
||||
mapping output.
|
||||
|
||||
No re-pin is needed outside library. Inside library, only the draft deepEqual at mapping.test:48 gains
|
||||
`supporting: []`, as the design says.
|
||||
|
||||
### Q1 departure (sage-2923) and the :73 re-pin (mos-1330 item 4)
|
||||
|
||||
survey.mjs:65 counts `supporting: 0` and formatSurvey prints it. T8 is the only added test
|
||||
(survey.test "C2-01d: with --mapping the survey counts supporting files").
|
||||
|
||||
mos-1330 item 4, packages/library/tests/survey.test.mjs:73, commit 28918824c:
|
||||
- old: `'scopes canon 5, playground 1, unmapped 3; indexed 6',`
|
||||
- new: `'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',`
|
||||
|
||||
N2: the pin gains only `supporting 0`. No other text on the line or in that test moved.
|
||||
|
||||
### Q2 tradeoff (webby-0169)
|
||||
|
||||
A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. That goes past
|
||||
the brief, but it is the fail-closed reading, and it uses an existing code: classify checks supporting
|
||||
first, so that playground prefix could never match. The cost is that she can't nest a playground under
|
||||
a supporting folder, though she can nest the other way (a playground holding a supporting folder).
|
||||
|
||||
### Handoffs
|
||||
|
||||
- Q4 (webby-0169, sage-2930): the M12 delete rule ("A delete also needs the path to exist at the tip as
|
||||
a 100644 blob", case2 DESIGN.md :308) is not in supportingPath. It binds row 288 (C2-02) via Gitea
|
||||
#1793 comment 30738, with tests for a missing path and a non-100644 entry.
|
||||
- A save with no `editor` refusing is C2-02's (DESIGN.md section 1).
|
||||
|
||||
### Choices the design left open
|
||||
|
||||
- The book loop is now `againstBooks(owners, list, code)` in mapping.mjs, called once per list. The
|
||||
playground's messages and codes are byte-equal to before.
|
||||
- A non-string path refuses `library-write-story-path` ("the path must be a string") as part of
|
||||
check 1. A non-array `tree` is a TypeError (DESIGN.md section 3).
|
||||
- Check 8 builds the tree's names as every path and every folder above it, keyed by
|
||||
`normalize('NFC').toLowerCase()`. A name on the way refuses if its key holds any name other than
|
||||
itself, so a tree that already holds case twins refuses a write to either (R7).
|
||||
|
||||
### Mutants (exact strings)
|
||||
|
||||
Each applies one replacement to the build tip. Run one at a time, and restore with `git checkout --`
|
||||
on the one file before the next.
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M1 | src/scope.mjs | `const prefix = entry.supporting.find(p => inside(p, path));` | `const prefix = entry.supporting.find(p => inside(p, path) && !entry.playground.some(q => inside(q, path)));` | scope.test "supporting comes before playground", supporting.test "passes at book level" (both-lists path) |
|
||||
| M2 | src/mapping.mjs | `if ((inside \|\| holding) && owner !== id)` | `if ((inside \|\| holding) && owner !== id && list !== 'supporting')` | mapping.test "each supporting prefix rule", business.test C2-04 |
|
||||
| M3 | src/supporting.mjs | `for (const p of own) if (LINKS.has(modes.get(p)))` | `for (const p of own) if (false && LINKS.has(modes.get(p)))` | supporting.test "symlink or gitlink" |
|
||||
| M4 | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.normalize('NFC');` | supporting.test "case or Unicode form" |
|
||||
| M4b | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.toLowerCase();` | supporting.test "case or Unicode form" (NFD case) |
|
||||
| M5 | src/supporting.mjs | `Buffer.byteLength(path, 'utf8') > MAX_BYTES` | `path.length > MAX_BYTES` | supporting.test "1025 bytes" |
|
||||
| M6 | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[<>]/;` | mapping.test "editor is optional" |
|
||||
| M6b | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f]/;` | mapping.test "editor is optional" |
|
||||
| M7 | src/supporting.mjs | `if (place.scope !== 'supporting' \|\| place.universe !== universe)` | `if (place.scope !== 'supporting')` | supporting.test "outside the universe's supporting folders" |
|
||||
| M8 | src/mapping.mjs | `if (sid === pid && s !== p && holds(s, p))` | `if (false && sid === pid && s !== p && holds(s, p))` | mapping.test "each supporting prefix rule" (Workshop case) |
|
||||
|
||||
(The `\|` in the table is an escaped `|` in the source text.) Each mutant runs the one test file named,
|
||||
plus business.test for M2, under `timeout -k 10 600`. A mutant passes the gate only when its run is red
|
||||
on the named test and the restored file's sha256 equals the build tip's.
|
||||
|
||||
### Gate plan (on Sage's CLEAR)
|
||||
|
||||
1. The touched files whole, each under `timeout -k 10 600`: library mapping, scope, supporting and
|
||||
survey tests, and business.test.mjs. Counts go below.
|
||||
2. M1-M8 as above.
|
||||
3. The full gate, under mos-1275 timeouts: `node --test packages/library/tests/` and
|
||||
`node --test packages/business/tests/` each under `timeout -k 10 1800`, and every scripts/test-*.sh
|
||||
under `timeout -k 10 300`. pipefail and tee, with skips named (test-task Docker groups).
|
||||
4. Before in-review: grep packages/*/tests for a 64-hex pin on any changed file and for the changed
|
||||
message text, then the drift check on BASE..refactor.
|
||||
|
||||
## 2026-10-23 Webby early look (webby-0227, static, not a verdict)
|
||||
|
||||
- E1: business.test.mjs is an existing test file with a 4-line addition after the playground refusal.
|
||||
The in-review request lists it next to survey.test:73.
|
||||
- E3 fixed: supportingPath now runs the string check before `JSON.stringify`, so a BigInt (or any
|
||||
non-string) path refuses `library-write-story-path` instead of throwing a TypeError.
|
||||
supporting.test's string case now covers 7, null, undefined, 1n and an array.
|
||||
- E2 open: `editor` takes surrounding whitespace, and git trims it at commit, so the saved ident and
|
||||
the commit ident can differ. DESIGN.md section 1 says "non-empty after trim". Refusing
|
||||
`value.trim() !== value` would change that approved rule, so it went to Sage instead of into the
|
||||
build.
|
||||
|
||||
## 2026-10-23 E2 departure (sage-3601)
|
||||
|
||||
A departure from DESIGN.md section 1 (which stays unedited). The design says each `editor` value is
|
||||
"non-empty after trim". Sage ruled that surrounding whitespace refuses too, because git trims an
|
||||
ident at commit, so a padded value would save one name and commit another. mapping.mjs now adds
|
||||
`if (value.trim() !== value) bad(`editor.${key} must not start or end with whitespace`)` after the
|
||||
IDENT_BAD check. `trim()` covers Unicode whitespace, so U+00A0 and U+3000 refuse as well. Inner
|
||||
whitespace is kept ("Her Middle Name" passes).
|
||||
|
||||
T3 gains a case per key for leading, trailing and both-ends spaces, U+00A0 and U+3000, plus one inner
|
||||
whitespace pass.
|
||||
|
||||
M6 grows a third part:
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value) bad(` | `if (false && value.trim() !== value) bad(` | mapping.test "editor is optional" (padded cases) |
|
||||
|
||||
Fixture grep: `git grep -nE "\beditor\b|\bauthor\b" -- 'packages/library/tests/*' 'packages/business/tests/*'`,
|
||||
leaving out mapping.test.mjs, finds nothing. packages/business/tests has no fixtures directory. The
|
||||
library fixture mapping.json has no `editor`. The positive control (`vaultPrefix` in mapping.json)
|
||||
matches once. No fixture maps an editor or author with surrounding whitespace.
|
||||
|
||||
## 2026-10-23 E4 (webby-0230, sage-3606), same departure as E2
|
||||
|
||||
Claimed: `queue move 283 in-progress`, op honker-283-start, rev 3642, 19:10:49Z (briefed→in-progress).
|
||||
|
||||
Git's ident.c `crud()` strips any byte <= 0x20 and `, : ; < > " \ '` from both ends of an ident. I
|
||||
checked that with git 2.55.0: `Jane,` `"Jane"` `'Jane'` `Jane;` `Jane:` `Jane\` and `,Jane` all
|
||||
commit as `Jane`, while `Jane.` and `Ja,ne` are kept. mapping.mjs now takes the set from crud()
|
||||
rather than from that list: `IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/`.
|
||||
The E2 check became `if (value.trim() !== value || IDENT_CRUD_END.test(value))`, which refuses
|
||||
library-mapping-invalid with "editor.<key> must not start or end with whitespace or any of , : ; " ' \".
|
||||
`trim()` still catches Unicode whitespace (U+00A0, U+3000), which crud() leaves alone.
|
||||
|
||||
Inner characters: IDENT_BAD (`/[\u0000-\u001f\u007f-\u009f<>]/`) already refuses `<`, `>`, newline,
|
||||
tab and every other control anywhere in the value. T3 now pins that at the start, the end and the
|
||||
middle. Inner `, : ; " ' \` are kept, since git keeps them, and T3 pins that too.
|
||||
|
||||
Tests:
|
||||
- T3 has one case per character in [space, U+00A0, U+3000, `,`, `:`, `;`, `"`, `'`, `\`], at the start
|
||||
and at the end, for each key. `<`, `>`, newline and tab are pinned at the start, end and middle.
|
||||
Four accepted names have inner crud characters.
|
||||
- New round-trip test, "an accepted editor round-trips through a real git commit unchanged". It
|
||||
commits in a scratch repo with editor `Doe, Jane Q.` / `[email protected].` as author and
|
||||
committer, with GIT_AUTHOR_* and GIT_COMMITTER_* stripped from the env, and asserts `git log`
|
||||
shows exactly the saved values. I preflighted that commit with git 2.55.0 by hand: both values came
|
||||
back unchanged.
|
||||
|
||||
M6c now replaces the whole end check (this supersedes the earlier M6c row):
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (false)` | mapping.test "editor is optional" (end cases) |
|
||||
| M6d | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (value.trim() !== value)` | mapping.test "editor is optional" (`,` `:` `;` `"` `'` `\` cases) |
|
||||
|
||||
## 2026-10-23 E5 (webby-0236, not blocking), fixed
|
||||
|
||||
The round-trip test stripped only GIT_AUTHOR_* and GIT_COMMITTER_*. With GIT_DIR, GIT_WORK_TREE or
|
||||
GIT_INDEX_FILE set (inside a hook, say), GIT_DIR beats `-C root` and the commit would land in the
|
||||
outer repo, and the test read global and system config. It now deletes GIT_DIR, GIT_WORK_TREE,
|
||||
GIT_COMMON_DIR and GIT_INDEX_FILE (as queue/tests/helpers.mjs gitEnv does) and sets
|
||||
GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 (as ledger.test.mjs does). The `-c user.*`
|
||||
ident still applies. Test only; no mutant changes.
|
||||
|
||||
## 2026-10-23 sage-2925: optional editor.user (sage-3649)
|
||||
|
||||
sage-2925 (recorded in ludwig/row286-r2 agents/ludwig/work/queue-286/BUILD.md, Q4) ruled that row 283
|
||||
adds an optional `editor.user`, her Gitea login for the `library-human` slot, which row 288 (C2-02)
|
||||
passes to `humanCredential(context, {user})`. It reached this row after the DESIGN.md d2 approval
|
||||
(ludwig-0068, bubba-0009, sage-3649), so it departs from DESIGN.md section 1 ("exactly the keys
|
||||
`name` and `email`"). DESIGN.md stays unedited.
|
||||
|
||||
- mapping.mjs: `only(entry.editor, ['name', 'email', 'user'], 'editor')`. When present, `user` must be
|
||||
a string matching USER (`/^[A-Za-z0-9_.-]{1,100}$/`, the entry `user` check), else
|
||||
library-mapping-invalid "editor.user must be a plain account name". The output carries `user` only
|
||||
when given. An absent `editor.user` refusing library-human-missing is 288's (bubba-0009).
|
||||
- README: the `editor` line names `user` and the end-character rule from E2/E4.
|
||||
- Tests: T3 (mapping.test "editor is optional") gains one accepted case (`her.login-1`, kept in the
|
||||
output) and refused cases for '', an inner space, '@', 101 characters, 7 and null. They go in
|
||||
mapping.test with the other editor cases, not in the new supporting.test, which tests write paths.
|
||||
- No test outside mapping.test reads `editor` or pins mapping.mjs or README (git grep).
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6e | src/mapping.mjs | `if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' \|\| !USER.test(entry.editor.user)))` | `if (false)` | mapping.test "editor is optional" (editor.user refusals) |
|
||||
| M6f | src/mapping.mjs | `...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {})` | `...({})` | mapping.test "editor is optional" (`her.login-1` kept) |
|
||||
|
||||
## 2026-10-23 T2 run (sage-3695 CLEAR, lane A, library and business only)
|
||||
|
||||
Node v26.8.1. refactor was e2b873390 (226 landed); `git diff --stat 45dca69f6 e2b873390 -- packages/library
|
||||
packages/business` is empty. TMPDIR was a mktemp -d under /mnt/storage/scratch/tmp, with pipefail.
|
||||
Singles and mutants ran under `timeout -k 10 600`, suites under `timeout -k 10 1800`.
|
||||
|
||||
First-run red (mos-1272, placed): the first base run on 46bad7ea3 at 20:01:09Z was red in
|
||||
supporting.test "malformed paths refuse by class, in order". 5 of 6 passed, with `TypeError: Do not know
|
||||
how to serialize a BigInt`. The cause was the test helper, not the product: `refuses()` labelled each
|
||||
assertion with `JSON.stringify(path)`, and the 1n case added for E3 can't be stringified. In c18ad3e12
|
||||
the label uses `util.inspect(path)`, a 2-line test-only change. The product's 1n refusal was never in
|
||||
question: supportingPath type-checks before it stringifies (E3). The whole run below is on c18ad3e12.
|
||||
|
||||
Base, the touched files whole (20:01:43Z), all rc=0:
|
||||
- library mapping.test 16/16, scope.test 6/6, supporting.test 6/6, survey.test 7/7;
|
||||
- business business.test 21/21;
|
||||
- 0 skipped, 0 cancelled, 0 todo.
|
||||
|
||||
Mutants (20:01:44Z). Each was applied by exact string with count 1, then restored with `git checkout --`.
|
||||
Every restored sha256 equals HEAD's blob, and the tree was clean after.
|
||||
|
||||
| Mutant | Run | Result | Failing test |
|
||||
|---|---|---|---|
|
||||
| M1 | scope.test | rc=1, 5/6 | supporting comes before playground; a folder in both lists is supporting… |
|
||||
| M1 | supporting.test | rc=1, 5/6 | a path passes at book level, universe level, in a draft and in a folder in both lists |
|
||||
| M2 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
|
||||
| M2 | business.test | rc=1, 20/21 | C2-04: library mappings load without roots, remain frozen, and retain mapping refusals |
|
||||
| M3 | supporting.test | rc=1, 5/6 | a symlink or gitlink on the way, or at the path itself, refuses |
|
||||
| M4 | supporting.test | rc=1, 5/6 | a name that differs from a tree name only in case or Unicode form refuses… |
|
||||
| M4b | supporting.test | rc=1, 5/6 | the same case/Unicode test |
|
||||
| M5 | supporting.test | rc=1, 5/6 | a path of exactly 1024 bytes passes, and 1025 bytes… refuses |
|
||||
| M6 | mapping.test | rc=1, 15/16 | editor is optional, kept when given, and refuses anything but a plain name and email |
|
||||
| M6b | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6c | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6d | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6e | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6f | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M7 | supporting.test | rc=1, 5/6 | a path outside the universe's supporting folders refuses |
|
||||
| M8 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
|
||||
|
||||
14 of 14 mutants killed, each on its named test only.
|
||||
|
||||
Suites (20:01:47Z):
|
||||
- `packages/library`: `node --test tests/*.test.mjs` rc=0, 98/98, 0 skipped.
|
||||
- `packages/business`: `node --test tests/` rc=0, 231/231, 0 skipped.
|
||||
|
||||
Not run here: every scripts/test-*.sh and the other packages. Those are Sage's 283 gate, after Webby's
|
||||
APPROVE.
|
||||
|
||||
## 2026-10-23 G1: mos-1528 OK for the mapping.test draft deepEqual (sage-3713)
|
||||
|
||||
This entry completes the N3 line above (BUILD.md:42, "only the draft deepEqual at mapping.test:48 gains
|
||||
`supporting: []`"). BUILD.md is append-only, so :42 stays as written. Mos OK'd the change of that old
|
||||
assertion in mos-1528. It is now at packages/library/tests/mapping.test.mjs:52, and commit 331653b38
|
||||
changed it. Everything else in that commit's hunk is new test lines, not changed old ones.
|
||||
|
||||
- Old, mapping.test.mjs:48 at 45dca69f6:
|
||||
`assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'] });`
|
||||
- New, mapping.test.mjs:52 at b2a1f4092:
|
||||
`assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'], supporting: [] });`
|
||||
|
||||
The line gains only `, supporting: []`; nothing else in it moved. The business.test and scope.test hunks
|
||||
add tests only (sage-3713: no OK needed). The other old-test change in this row is survey.test:73
|
||||
(mos-1330 item 4, 28918824c). Doc only.
|
||||
@@ -1,191 +0,0 @@
|
||||
# Row 283 (#1788) C2-01d design, draft 2 (Honker)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at
|
||||
agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300),
|
||||
1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The
|
||||
build waits for 282 to land (sage-2906).
|
||||
|
||||
## 1. Mapping (packages/library/src/mapping.mjs)
|
||||
|
||||
`supporting`:
|
||||
- Every universe kind takes an optional `supporting` list: plain `['name','books','playground','supporting']`,
|
||||
hybrid adds it to its seven keys, and a draft gets `['kind','name','playground','supporting']`.
|
||||
- Each value is checked as a playground prefix is: a string ending in `/` whose body passes `folder()`.
|
||||
- The output carries `supporting` on every universe, `[]` when absent, the way a draft carries
|
||||
`books: []`. classify then needs no `?? []`. The business writer writes the raw document
|
||||
(organization.mjs:195), not this output, so no business file changes shape.
|
||||
- The prefix rules, across every entry (ruling after r11):
|
||||
- A supporting prefix holding another supporting prefix, or one listed twice, refuses
|
||||
`library-mapping-invalid`.
|
||||
- A prefix equal to one of its own entry's book folders, or holding one, refuses `library-mapping-invalid`.
|
||||
- A prefix inside or holding another entry's book folder refuses `library-supporting-other-universe`.
|
||||
- Level comes from where the prefix sits, as for the playground: inside one of its own entry's books
|
||||
is book level, and outside every book is universe level. A draft's prefixes are all universe level.
|
||||
- Between the two lists (my choice, Q2):
|
||||
- A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
|
||||
- A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting
|
||||
and the rest stays playground.
|
||||
- A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. classify
|
||||
checks supporting first, so that playground prefix could never match.
|
||||
- A supporting prefix equal to, holding or inside a playground prefix of another entry refuses
|
||||
`library-mapping-invalid`. Otherwise one path would have two universes.
|
||||
- The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and
|
||||
label. The playground's messages and codes stay byte-equal, so no existing test text moves.
|
||||
|
||||
`editor`:
|
||||
- The entry takes an optional `editor`. When present, it is an object with exactly the keys `name` and
|
||||
`email` (via `only()`). Each value is a string that is non-empty after trim and has no control
|
||||
character (`[\u0000-\u001f\u007f-\u009f]`), `<` or `>` (webby-0169 R1: git drops them from an ident
|
||||
and commits under a different name, so C2-02 would never see a failure). Anything else refuses
|
||||
`library-mapping-invalid`.
|
||||
- The output carries `editor: { name, email }` only when present, as `user` and `vaultPrefix` do. A save
|
||||
with no `editor` refusing is C2-02's job.
|
||||
|
||||
## 2. Scope (scope.mjs)
|
||||
|
||||
- `classify` gets a supporting loop before the playground loop (design 1.2). It returns `place(...)`
|
||||
with `{ scope: 'supporting', universe, book }`. The book is the book folder holding the prefix, or
|
||||
null at universe level and in a draft. The type and folder come from the first folder under the
|
||||
prefix, as for the playground.
|
||||
- The `.md` check stays first, so a `.canvas` or `.base` under a supporting prefix stays unmapped.
|
||||
- The comment above `classify` names the three prefix scopes and the order.
|
||||
|
||||
## 3. supportingPath (new supporting.mjs)
|
||||
|
||||
`supportingPath(mapping, universe, path, tree)` returns `path` or throws
|
||||
`LibraryError('library-write-story-path', ...)`. It is pure and reads no git.
|
||||
|
||||
- `tree` is the array `listTree` returns (reader.mjs:26, `{ mode, type, oid, size, path }`), so the
|
||||
C2-02 broker passes its listing as it is (Q3). A non-array `tree` throws a TypeError, because that is
|
||||
a caller bug, not a path class.
|
||||
- The checks run in this order, each with its own message naming the class:
|
||||
1. The path must be a string of at most 1,024 UTF-8 bytes (`Buffer.byteLength`).
|
||||
2. It must have no control character, using the same control class as `editor` (not `<` or `>`).
|
||||
3. It must have no backslash.
|
||||
4. It must not be absolute (a leading `/`).
|
||||
5. It must have no empty, `.` or `..` segment. That also catches `//` and a trailing `/`.
|
||||
6. `classify(mapping, path)` must give scope `supporting` and that same `universe`. This one check
|
||||
covers an unknown universe, an unmapped path, a `.canvas`, `.base` or attachment, a canon or
|
||||
story path, a playground path that isn't also supporting, and another universe's prefix. The scope
|
||||
rule then has one source, and M1 below hits both.
|
||||
7. Every proper prefix of the path (`a`, `a/b`, ...) and the path itself must not be in `tree` with
|
||||
mode `120000` (symlink) or `160000` (gitlink).
|
||||
8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory
|
||||
(derived from the entries' paths) after `normalize('NFC').toLowerCase()` on both sides, unless
|
||||
it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of
|
||||
a name as one file (webby-0169 N1).
|
||||
Directories are checked too, because the folders under a supporting prefix are hers to name. A
|
||||
path in a tree that holds both `a.md` and `A.md` refuses, which matches R7.
|
||||
- The refusal messages quote the path with JSON, except for the control-character class, whose message
|
||||
leaves the path out.
|
||||
- The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in
|
||||
the brief's supportingPath list. It stays with C2-02's delete (Q4).
|
||||
The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs
|
||||
(webby-0169 Q4).
|
||||
- index.mjs exports `supportingPath`.
|
||||
|
||||
## 4. README (packages/library/README.md)
|
||||
|
||||
- Mapping: `supporting` and its rules, the cross-list rules, and `editor`.
|
||||
- Refusals: a `library-supporting-other-universe` row, and the new cases in the
|
||||
`library-mapping-invalid` row.
|
||||
- Scope: supporting first, then playground, then canon.
|
||||
- A new section, "Agent write paths", for supportingPath and its classes.
|
||||
|
||||
## 5. Callers of classify outside files owned (Q1 for Sage)
|
||||
|
||||
- survey.mjs:65 builds `{ canon: 0, playground: 0, unmapped: 0 }` and increments
|
||||
`scopes[classify(...).scope]`. A supporting file adds `supporting: NaN`, and formatSurvey (:92) never
|
||||
prints it. I propose a departure: add `supporting: 0` and print it, so the line becomes
|
||||
`scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6`. That re-pins survey.test:73 and
|
||||
adds a count test. Without it, the survey reports garbage once she marks a folder. The departure
|
||||
covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
|
||||
- No change in the rest, each for a reason:
|
||||
- reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
|
||||
- R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file
|
||||
raises nothing.
|
||||
- render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
|
||||
- mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
|
||||
- No bus or webui source filters on scope (git grep at 94c33a43c).
|
||||
- M9 already protects the whole `projects` key from agent edits (library-projects-protected.test). No
|
||||
test is added there.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- mapping.test:
|
||||
- T1: `supporting` at book level, at universe level, in a hybrid and in a draft. The output carries
|
||||
it, with `[]` when absent. The draft `deepEqual` at :48 gains `supporting: []`.
|
||||
- T2: one test per prefix rule.
|
||||
- These refuse `library-mapping-invalid`: no trailing `/`, a bad folder, one holding another, one
|
||||
listed twice, one equal to its own book, one holding its own book, one holding a playground
|
||||
prefix, and one overlapping another entry's playground.
|
||||
- These refuse `library-supporting-other-universe`: one inside another universe's book, and one
|
||||
holding another universe's book.
|
||||
- An equal same-entry playground passes.
|
||||
- T3: `editor` passes, and absent passes. It refuses `library-mapping-invalid` for a non-object, an
|
||||
array, an extra key, a missing key, an empty or blank value, a non-string, and a control character,
|
||||
`<` or `>` in either value.
|
||||
- scope.test, T4:
|
||||
- Supporting at book level, universe level and in a draft each give their universe and book.
|
||||
- A folder in both lists is `supporting`, and a playground-only folder is `playground`.
|
||||
- A supporting `.canvas` or `.base` is unmapped.
|
||||
- supporting.test (new):
|
||||
- T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
|
||||
- T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant.
|
||||
The classes are:
|
||||
- unmapped, `.canvas`, `.base`, an attachment, canon, playground-only, another universe's
|
||||
supporting prefix, and an unknown universe;
|
||||
- absolute, `..`, `.`, `//`, a trailing `/`, a backslash, a C0 control and DEL;
|
||||
- a symlink folder, a gitlink folder, and a symlink at the path itself;
|
||||
- a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
|
||||
- 1,025 bytes made with a multi-byte character, so bytes and characters differ.
|
||||
- Each asserts the code `library-write-story-path`.
|
||||
- business.test: T7 adds one assertion that `validateBusinessDocument` refuses
|
||||
`library-supporting-other-universe`.
|
||||
- survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.
|
||||
|
||||
## 7. Mutants (exact strings go in BUILD.md)
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | classify checks playground before supporting (the brief's mutant) | T4 both-lists, T5 both-lists |
|
||||
| M2 | the other-universe fail for supporting is dropped | T2 |
|
||||
| M3 | the symlink and gitlink check is dropped | T6 |
|
||||
| M4 | the case check is dropped | T6 |
|
||||
| M4b | check 8 drops `normalize('NFC')` | T6 NFD variant |
|
||||
| M5 | the byte length uses `path.length` | T6 1,025 bytes |
|
||||
| M6 | the `editor` control-character check is dropped | T3 |
|
||||
| M6b | the `editor` `<` and `>` check is dropped | T3 |
|
||||
| M7 | supportingPath skips the universe match | T6 other universe |
|
||||
| M8 | the supporting-holds-playground check is dropped | T2 |
|
||||
|
||||
## 8. Gate
|
||||
|
||||
- Webby approves the manifest.
|
||||
- The library and business suites and every scripts/test-*.sh are green on Sage's gate.
|
||||
- Timeouts (mos-1275): each package `timeout -k 10 1800`, each script `timeout -k 10 300`, each single
|
||||
file and mutant `timeout -k 10 600`. These come from my row 256 gate at 10:34Z.
|
||||
- Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed
|
||||
message text, then drift-check BASE..refactor.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
- Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
|
||||
- Q2 (Webby): the cross-list rules in section 1?
|
||||
- Q3 (Webby): `tree` as the `listTree` array?
|
||||
- Q4 (Webby): the delete's 100644 check stays with C2-02?
|
||||
- Q5 (Webby): `editor` refuses only blank values and control characters, as the brief says. `<` and `>`
|
||||
would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.
|
||||
|
||||
## 10. Draft 2 (webby-0169)
|
||||
|
||||
- R1 folded: `editor` refuses `<` and `>` too, with T3 cases and mutant M6b.
|
||||
- Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground
|
||||
prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost
|
||||
is that she can't nest a playground under a supporting folder, though she can nest the other way.
|
||||
- Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
|
||||
- N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
|
||||
- N2: the survey.test:73 pin gains only `supporting`, and the count test is the only addition
|
||||
(mos-1330, its own commit).
|
||||
- N3: before the build, re-grep tests outside packages/library for a `deepEqual` of the mapping output
|
||||
on the build base.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Lena
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 296 (FED R7, handed over from Quackerjack).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread db27faca) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `lena/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,177 @@
|
||||
# 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.
|
||||
@@ -1,237 +0,0 @@
|
||||
# Row 216 build notes (C2-05a, lookup index and queries, #1717)
|
||||
|
||||
Phase A only: code, tests and this file. No test, suite or mutant has been run. `node --check` passed on every changed `.mjs`.
|
||||
|
||||
- Branch: `researcher/row216`
|
||||
- Base: `d372102882d6858fa77af4aee453f866c886ad5c` (origin/refactor)
|
||||
- Design: `agents/gosalyn/work/case2/DESIGN.md` on origin/gosalyn/case2-design, section 3 (table at lines 625-631, bullets 633-641), 1.2-1.4, B12, D2
|
||||
- Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-05a at lines 320-357
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Status |
|
||||
|---|---|
|
||||
| `packages/library/src/index-build.mjs` | new |
|
||||
| `packages/library/src/query.mjs` | new |
|
||||
| `packages/library/src/index.mjs` | edit: 2 export lines added |
|
||||
| `packages/library/tests/query-fixture.mjs` | new, test helper |
|
||||
| `packages/library/tests/query.test.mjs` | new |
|
||||
| `packages/library/tests/index-build.test.mjs` | new |
|
||||
| `agents/researcher/work/queue-216/BUILD.md` | new (this file) |
|
||||
|
||||
No existing test or source file changed beyond `index.mjs`.
|
||||
|
||||
## Design rows and where each is tested
|
||||
|
||||
| Design row | Code | Tests (query.test.mjs unless noted) |
|
||||
|---|---|---|
|
||||
| `library_find` | `find` | find is bound to its universe; every result carries its scope; long list pages; large items page under the cap |
|
||||
| `library_get` | `get` | hybrid gets a source file; get lists visible links both ways; get pages its lines |
|
||||
| `library_search` | `search` | search returns scoped line hits; item that cannot fit alone is refused |
|
||||
| `library_manuscript` | `manuscript` | one chapter at most 400 lines; refused without chapter; hybrid reads a source chapter |
|
||||
| `library_universe` | `universeInfo` | universe info reports a hybrid's sources, constraints, notes; unknown universe refused |
|
||||
| Hybrid semantics (1.4) | `sees` | a hybrid sees its sources canon minus exclusions, no playground |
|
||||
| Unmapped never appears | index build + `sees` | unmapped files never appear (query.test); index holds mapped files only (index-build.test) |
|
||||
| Per-commit cache (3) | `indexFor` | same commit, path, mapping return one index; different path, mapping or commit get their own; eight-entry bound; failed read not kept; unchecked mapping refused (index-build.test) |
|
||||
| Rebuild time (3, C2-05a gate) | `indexFor` | 3,000-file rebuild, time reported with `t.diagnostic`, no threshold asserted |
|
||||
| REPLY_CAP parity | `QUERY_REPLY_CAP` | equals `REPLY_CAP` from harness tools.mjs |
|
||||
|
||||
## Choices where the design table is silent or differs
|
||||
|
||||
These are departures or gaps. Each is built as stated, so Sage can rule before Phase B. A ruling that changes one of them is a small edit.
|
||||
|
||||
1. **Paging arguments (gap a).** The table has no offset. `find` and `search` take an optional `offset` (a position in the filtered list, default 0). A reply carries `next`: the position of the next item, or `null`.
|
||||
2. **`library_get` has no universe (gap b).** Built with a required `universe`. Without it the reply could not be isolated. A missing file and a file the universe cannot see both return `null`, so a caller cannot tell them apart. `get` also takes an optional `from` line.
|
||||
3. **`get` over the cap (gap c).** Lines page by `from` with `next`. `links_out` and `links_in` come back whole on every page. If the frontmatter and links alone exceed the cap, the call is refused with `query-item-too-large`. A hub note with many backlinks may be refused. This needs a ruling: link paging, or accept the refusal.
|
||||
4. **`manuscript` without a chapter (gap d).** Refused with `query-chapter-required`. The design names no list form. A chapter name that matches two visible files is refused with `query-chapter-ambiguous`.
|
||||
5. **`search` excerpt (gap e).** The excerpt is the whole line, not a truncation. A line that alone exceeds the cap is refused. Truncating it would cut an item.
|
||||
6. **`exclude` form (gap f).** `checkMapping` accepts only a folder path for `exclude` (`folder()`), so an exclusion is a path prefix. There is no type or source form to build. Confirm.
|
||||
7. **Pin constraints.** `pin` does not hide any file. It is reported by `universeInfo` only. Confirm.
|
||||
8. **Playground through `get`.** `scope` applies to lists. `get` by an explicit path returns a playground file of the caller's own universe. Confirm.
|
||||
9. **Links.** A link into a file the universe cannot see is dropped from `links_out` and `links_in`, in line with N1. Links from a visible file that point into another universe are kept in the index (the reader keeps them under R6), and the query layer drops them for universes that cannot see them.
|
||||
10. **`find` matching.** Case-insensitive substring over the front matter `name` and the file name. An empty `query` matches every file in scope.
|
||||
11. **Cache key.** `repo`, `commit` and a sha256 of the canonical mapping. Keying by commit alone would return the wrong scopes after a mapping change. The cache holds eight entries, least recently used out. Failed reads are not kept.
|
||||
12. **`universeInfo`.** Reads only the checked mapping and the served commit. It builds no index.
|
||||
13. **Reply commit.** Every reply carries the index commit, which is the served commit passed in.
|
||||
|
||||
## Provisional refusal codes (pending Sage approval)
|
||||
|
||||
The brief says new refusal codes need Sage's approval first. These are provisional and must not be treated as settled:
|
||||
|
||||
- `query-argument-invalid`
|
||||
- `query-universe-unknown`
|
||||
- `query-chapter-required`
|
||||
- `query-chapter-ambiguous`
|
||||
- `query-item-too-large`
|
||||
|
||||
Reused, existing codes: `library-mapping-invalid`, plus the reader's `library-git`, `library-commit` through `indexFor`.
|
||||
|
||||
## Grep results (grep only, not a test run)
|
||||
|
||||
- Test references to the changed paths: `packages/library/tests/*.mjs` import `../src/index.mjs` and `../src/mapping.mjs`. `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs` import the library index as a namespace. Neither enumerates its exports, so the two new export lines break no assertion.
|
||||
- `packages/cli/tests/stack-copy.test.mjs:52` checks that `packages/library/src/index.mjs` exists, and it still does.
|
||||
- 64-hex pins: none in `packages/library/tests`, none on `index.mjs`, `query.mjs` or `index-build.mjs`.
|
||||
- `scripts/test-*.sh`: no reference to the new modules.
|
||||
|
||||
## Flag for Sage
|
||||
|
||||
- `packages/library/tests/helpers.mjs` `repo()` builds its fixtures under `tmpdir()`, which is `/tmp`. The launcher rules forbid that. The new tests do not use it; they use `tests/query-fixture.mjs`, which builds under `/mnt/storage/scratch/tmp`. The existing helper should be fixed in a separate row, not here.
|
||||
|
||||
## Phase B commands (not run)
|
||||
|
||||
Run only after Sage sends "slot clear". Set the scratch TMPDIR first, per the launcher rule.
|
||||
|
||||
```
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-aa3cc98787bc5152c/packages/library
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
node --test tests/query.test.mjs tests/index-build.test.mjs
|
||||
node --test tests/*.test.mjs
|
||||
```
|
||||
|
||||
Also run `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs`, since they import the index.
|
||||
|
||||
## Status
|
||||
|
||||
Not yet verified. Nothing has been tested and nothing is committed. The working tree holds the Phase A changes. AGENTS.md invariant 8 allows a commit only after the applicable suites are green, so the commit waits for Phase B. The dispatch asked for a local commit in Phase A; Sage to confirm that the commit waits for Phase B.
|
||||
|
||||
## Round 2 (2026-10-22, sage-2708, sage-2716): rulings applied, rebased onto aca16404c
|
||||
|
||||
Added after the text above; nothing above is edited. "Nothing is committed", "Provisional refusal codes",
|
||||
`query-chapter-required` and the tmpdir flag above were true when written and are superseded here.
|
||||
|
||||
- Branch `researcher/row216-r2`, one local commit on origin/refactor `aca16404c`. Row 268 (C2-01c) is in
|
||||
that base. Row 253 has not landed; the work rebases onto 253's tip before any gate. Not pushed, not run.
|
||||
- Rulings: `agents/sage/work/queue-216/RULINGS.md` at da9555ca2, rulings 1-13 and the tmpdir flag.
|
||||
- Drift check, d37210288..aca16404c against my paths: the library files that changed are `mapping.mjs`,
|
||||
`scope.mjs`, `README.md` and the mapping and scope tests. None is a file I add or edit. `query.mjs` reads
|
||||
the checked mapping through them, so the whole library package runs in phase B.
|
||||
|
||||
### What changed from phase A
|
||||
|
||||
| Ruling | Change in `query.mjs` | Test |
|
||||
|---|---|---|
|
||||
| 3 `get` over the cap | `links_out` and `links_in` page together with `links_from`; every reply carries `links_next`, `next` and the front matter. A page with lines left carries at least one line. Links take at most half the cap while lines remain. `query-item-too-large` is left for front matter alone or one line alone (or one link alone) | hub note with 900 backlinks pages and adds up; front matter alone and a line alone refused; bad `links_from` |
|
||||
| 4 `manuscript` no chapter | `chapter` optional: the first visible chapter in path order. Every reply carries `next_chapter`. `query-chapter-required` removed. Ambiguity still refuses | first chapter and `next_chapter`; ambiguous name refused |
|
||||
| 5 `search` long line | hit comes back `{path, line, excerpt: null, excerpt_too_long: true, scope}`, other hits still return | marked hit and unmarked hits in one reply |
|
||||
| 6 `exclude` | path-prefix reading only. The test names `EXCLUSION` and `EXCLUDED_PATHS` in `query-fixture.mjs`, so C2-15 (row 267) swaps two constants | exclusion hides from the hybrid and not from the source |
|
||||
| 8 hybrid `get` | unchanged code; test added | hybrid gets a source's playground file as `null`; the source gets it with `scope: "playground"` |
|
||||
| PRD 1.14 (row 268) | `universeInfo` reports `kind` from the mapping (`draft` included) | four tests below |
|
||||
|
||||
PRD 1.14 tests, one each: a lookup naming a `book` leaves `book: null` files out (find and search);
|
||||
`library_manuscript` never reads a `book: null` file; a draft universe has no canon, so its lookups return
|
||||
files only with `scope: "playground"`; `library_universe` on a draft returns kind, name and prefixes.
|
||||
The levels fixture is `WITH_LEVELS` in `query.test.mjs`: harbor gets `Notes/Harbor/` as a universe-level
|
||||
playground prefix, and `idea` is a draft over `Drafts/New World/`.
|
||||
|
||||
`query-fixture.mjs` builds under `tmpdir()`, as `helpers.mjs` does (ruling "tmpdir flag"). The earlier
|
||||
flag about `helpers.mjs` building under `/tmp` is closed by that ruling: the gate sets `TMPDIR`.
|
||||
|
||||
Refusal codes now: `query-argument-invalid`, `query-universe-unknown`, `query-chapter-ambiguous`,
|
||||
`query-item-too-large`. All four approved in the rulings.
|
||||
|
||||
### Grep results (grep only, not a test run)
|
||||
|
||||
- `git grep` over `packages/*/tests/*` and `scripts/*` for `query.mjs` and `index-build`: no hit outside
|
||||
`packages/library/tests/query*.test.mjs` and `index-build.test.mjs`.
|
||||
- 64-hex pins naming a library path: none. Positive control: the same pathspec finds the sha256 lines in
|
||||
`packages/cli/tests/case1-hostcheck.test.mjs` and the `packages/library/src/index.mjs` existence check at
|
||||
`packages/cli/tests/stack-copy.test.mjs:52`.
|
||||
- Tests that read `packages/library/src` as a directory: `packages/cli/tests/stack-release-fixture.mjs:309`
|
||||
copies `packages/library/src` whole into a release fixture. It carries no file list, so two new files break
|
||||
no assertion. `stack-release.test.mjs` is not in the gate unless Sage wants it.
|
||||
- Tests that import the library index as a namespace: `packages/bus/tests/library.test.mjs` and
|
||||
`library-sync.test.mjs`. Neither enumerates exports, so the two export lines add no break. Both run
|
||||
whole in phase B.
|
||||
|
||||
### Run limits (fixed before any run)
|
||||
|
||||
Every node file runs as its own `setsid -w timeout -k 10 LIMIT node --test FILE`; nothing runs unbounded.
|
||||
|
||||
- Node test files: LIMIT 900 s. The library test files have no recorded time. 900 s is above the longest
|
||||
recorded whole-package run I know of (cli, 591 s, row 281 BUILD.md "Runs" step 3), so it holds the 3x rule
|
||||
for any single file. The first run records each file's time, and a file past 300 s gets a limit re-derived
|
||||
from it before the next run. `query.test.mjs` builds a 900-file repo and a 3,000-file index rebuild is in
|
||||
`index-build.test.mjs`; both are the files most likely to need it.
|
||||
- No shell script is in this row's gate, so no script limit applies.
|
||||
|
||||
Any timeout goes to Sage, then Mos, unplaced (mos-1275).
|
||||
|
||||
### Phase B (not run; Sage's CLEAR first, then a rebase onto 253's tip)
|
||||
|
||||
```
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
cd <worktree>/packages/library
|
||||
for f in tests/*.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../bus
|
||||
for f in tests/library.test.mjs tests/library-sync.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../cli
|
||||
setsid -w timeout -k 10 900 node --test tests/stack-copy.test.mjs
|
||||
```
|
||||
|
||||
Mutants I propose (my list, not read off the brief; each is meant to be killed by the named test, and none
|
||||
has been thrown; the kill claims are unverified until CLEAR):
|
||||
|
||||
- M-a: `sees()` returns true for a file of another universe: killed by "find is bound to its universe".
|
||||
- M-b: the playground filter dropped: killed by "playground appears only when asked for".
|
||||
- M-c: the hybrid reads a source's playground: killed by "a hybrid gets a source playground file as null".
|
||||
- M-d: `links_next` always null: killed by the hub paging test.
|
||||
- M-e: the book filter lets `book: null` through: killed by the PRD 1.14 book test.
|
||||
|
||||
### Status
|
||||
|
||||
Built, not run. `node --check` has not been run on this round's files (no node before the CLEAR); the
|
||||
phase A pass above covered the earlier text only. The commit is local and waits for no gate under Sage's
|
||||
sage-2716 instruction, which allows git and docs work only; push and claim wait for Sage's go.
|
||||
|
||||
## Round 3: rebase onto 45dca69f6, drift check, hash-pin grep (2026-10-23T18:53:43Z, sage-3576)
|
||||
|
||||
Appended; nothing above is edited. Git only; nothing run, nothing pushed. Branch researcher/row216-r4 holds the
|
||||
one row 216 commit on 45dca69f6 (origin/refactor, 10-23). Earlier base: 48e450ce8 (row 253).
|
||||
|
||||
- Rebase: clean. `git range-diff 48e450ce8..038089e5f 45dca69f6..researcher/row216-r4` shows the commit as `=`.
|
||||
- Landed in 48e450ce8..45dca69f6: 1175 files. Path overlap with this row's 7 paths: none. packages/library: no
|
||||
landed file. packages/harness/src/tools.mjs (imported by query-fixture.mjs or the tests): not landed.
|
||||
- Hash-pin grep (the item 216 owed): 64-hex in packages/library: none. Positive control: 364 files in
|
||||
packages/*/tests and scripts match 64-hex. Tests and scripts that name library files by path:
|
||||
bus/tests/library.test.mjs and library-sync.test.mjs and bus/src/runtime.mjs import library/src/index.mjs;
|
||||
business/src/business.mjs and its test import library/src/mapping.mjs (unchanged by this row);
|
||||
cli/tests/stack-copy.test.mjs, stack-release-fixture.mjs:310 and stack-release.test.mjs:1991 copy
|
||||
packages/library/src as a tree; scripts/stack-copy.sh:37 copies it; scripts/mosaic:41 runs survey.mjs. None pins
|
||||
a hash of index.mjs, index-build.mjs or query.mjs.
|
||||
- bus/src/runtime.mjs landed changes after 48e450ce8 and uses libraryModule by named members (ensureMirror, SLOT,
|
||||
readLibraryFile, libraryStatus, sync, LibraryError, SYNC_INTERVAL_MS) only; the landed diff touches none of them,
|
||||
and this row's two new index.mjs exports (query and index build) are not iterated by any caller.
|
||||
- Gate additions for phase B: run bus library.test.mjs and library-sync.test.mjs whole (they import index.mjs), and
|
||||
the cli stack-copy and stack-release tests that copy packages/library/src.
|
||||
|
||||
## Round 4: phase B on 4e5d28ab2, two test fixes for r5 (2026-10-23T20:20:51Z, sage-3702, mos-1528)
|
||||
|
||||
Appended; nothing above is edited.
|
||||
|
||||
Phase B receipts on 4e5d28ab2 (researcher-r216-kit/out-b1): library whole 114 pass, 2 fail; bus library.test and
|
||||
library-sync.test 12 pass; cli 266 pass. The 3,000-file rebuild took 3807 ms (the test, with fixture setup, 4070 ms;
|
||||
`t.diagnostic` in index-build.test.mjs). No row 216 mutant was required or thrown in that round.
|
||||
|
||||
The 2 library reds are test faults, not product faults. Both are placed as first-run reds in files this row adds
|
||||
(mos-1272); Mos ruled both fixes in mos-1528 and Sage relayed it in sage-3712.
|
||||
|
||||
- R4: `packages/library/tests/index-build.test.mjs`, test "a different repo path, mapping or commit gets its own
|
||||
index". Old: `writeFileSync(join(repo, 'Book One/Places/New.md'), …)` with no `Book One/Places` folder in FILES, so
|
||||
the write failed with ENOENT. New: one `mkdirSync(join(repo, 'Book One/Places'), { recursive: true })` before the
|
||||
write (import line gains `mkdirSync`). No assertion changes.
|
||||
- R5: `packages/library/tests/query.test.mjs`, test "a hybrid sees its sources canon, minus exclusions, and no
|
||||
playground". The assertion sorts the actual paths and compared them to an unsorted literal, so the literal had
|
||||
the wrong order (Characters before Chapters; Second World Characters before Chapters). New: only the expected
|
||||
literal is reordered into sorted order. The `.sort()` on the actual side stays, and the assertion still checks
|
||||
order. The set of seven paths is unchanged.
|
||||
- Product code: no edit.
|
||||
|
||||
Reruns owed after the r5 commit (run on Sage's CLEAR, after the 283 gate): index-build.test.mjs whole,
|
||||
query.test.mjs whole, library whole, and the mutants those two files kill (M-a to M-e above). Results are appended
|
||||
below as a new entry.
|
||||
|
||||
Run limit note: stack-release.test.mjs ran 644 s in the cli run. A per-file limit of 3 x 644 = 1932 s covers it.
|
||||
Sage's gates run cli as one whole-package `node --test` under 2100 s.
|
||||
|
||||
Carry notes from Filbert's static check (doc only, no change in r5): N1 and N2 stay recorded as he gave them. The
|
||||
`After` list in this file still names row 268; row 268 landed at c27207f6e.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Row 220: C2-10a, no raw session view (M7) and no raw Discord engine (M8)
|
||||
|
||||
Phase A. No test, suite or mutant has been run. Sage opens the slot.
|
||||
|
||||
- Owner: researcher seat (Haiku 5.5 subagent), dispatched by Sage under lead decision 129.
|
||||
- Reviewer: Filbert.
|
||||
- Branch: `researcher/row220`, from `origin/refactor` at d37210288.
|
||||
- Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-10a (line 495) and "Rules every row keeps".
|
||||
- Design: `agents/gosalyn/work/case2/DESIGN.md` on `origin/gosalyn/case2-design`, sections 5.1 (M7, M8), 7.6, 8 (row list).
|
||||
|
||||
## Status
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| M7 reader side (`conversation-not-listed`) | Built. Tests written, not run. |
|
||||
| M7 production wiring (the caller that passes `isCreative`) | Not built. Outside the file list, and it needs a ruling (question 1). |
|
||||
| M8 `discord check` and `discord run` refusals | Not built. `packages/discord/src/cli.mjs` is untouched. Blocked on question 1 and question 2. |
|
||||
|
||||
Phase A is therefore partial. The row cannot pass its acceptance until Sage rules on the questions below.
|
||||
|
||||
## Files
|
||||
|
||||
- `packages/conversation/src/reader.mjs`: exports `NOT_LISTED`; `createReader` takes `isCreative` (default `() => false`); `catalogue()`, `open()` and `next()` hide a root for which it returns truthy.
|
||||
- `packages/conversation/tests/reader.test.mjs`: three new tests, inserted before the "unknown conversations" test.
|
||||
- `agents/researcher/work/queue-220/BUILD.md`: this file.
|
||||
|
||||
Nothing else is changed. `node_modules` in the worktree is a symlink to the canonical checkout and is ignored by git.
|
||||
|
||||
## Brief and design items mapped to tests
|
||||
|
||||
| Item | Test |
|
||||
|---|---|
|
||||
| M7: a creative session file under the root isn't listed | "M7: a creative seat's session is not listed and won't open by id; an assistant seat still lists" |
|
||||
| M7: and won't open by id (`conversation-not-listed`) | same test; second check of the open refusal in "M7: a creative session already open refuses its next page once the root is hidden" |
|
||||
| M7: an assistant session still lists | same test (the `sage` seat is listed and opens with its text) |
|
||||
| M7: an already-issued cursor stops once the root is hidden (my addition; the design's "won't open" implies it) | "M7: a creative session already open refuses its next page once the root is hidden" |
|
||||
| M7: refused rows and the unsupported placeholder of a creative root are hidden too (my addition; the design hides "creative sessions", and a refused row or placeholder names one) | "M7: a creative root's refused files and its unsupported placeholder are hidden as well" |
|
||||
| Default behavior without `isCreative` is unchanged | the existing reader suite; the default predicate returns false |
|
||||
| M8: `discord check` / `discord run` refuse `discord-engine-not-allowed` on a library business, and a `mosaic-bus` binding with `tools` | none (not built, question 1) |
|
||||
| M8: a pi binding on a business without a library still checks clean | none (not built, question 1) |
|
||||
|
||||
Every test's fixture text is `CREATIVE-MARKER` or a made-up assistant line. The M7 tests assert the marker is absent from the catalogue and refusal output.
|
||||
|
||||
## Choices made in phase A
|
||||
|
||||
- `isCreative(root)` is the reader's only input. The reader reads no business file. A truthy answer hides, so a doubtful answer fails closed. The predicate must depend on `root.seat` and `root.project` only, because a cursor is checked against the root object from an earlier listing.
|
||||
- Hidden check placed after the cursor's actor/purpose/conversation/branch checks in `next()`, and before the source is read.
|
||||
- `open()` checks hidden after the id resolves, so a creative id gets `conversation-not-listed`, and an unknown id still gets `unknown-conversation`.
|
||||
- `conversation-not-listed` is `reconcile: false`. The design names no reconcile flag. `unknown-conversation` uses true. Question 3.
|
||||
- The default predicate is `() => false`. Until a caller passes one, M7 does nothing. This is the open risk, and question 1 covers it.
|
||||
|
||||
## Grep over `packages/*/tests` and `scripts/test-*.sh` (static, phase A)
|
||||
|
||||
Changed path `packages/conversation/src/reader.mjs`:
|
||||
- `packages/conversation/tests/reader.test.mjs:14` imports `rootsFromSpecs, createReader, conversationId, cwdInProject, UNSUPPORTED_HARNESS`. It is the file under change.
|
||||
- `packages/control-board/tests/serve.test.mjs:26` imports `UNSUPPORTED_HARNESS`. That export is unchanged.
|
||||
- `packages/library/tests/sync.test.mjs:15` imports `packages/library/src/reader.mjs`, a different file. Not a hit.
|
||||
- `scripts/test-*.sh`: no reference to the conversation reader.
|
||||
- 64-hex pins naming this path: none. `packages/bus/tests/grant-migration.test.mjs:24` has a 64-hex value, but it is a DDL digest, not a pin on this path. That file matched only the word "reader" in a test title.
|
||||
|
||||
Changed path `packages/conversation/tests/reader.test.mjs`:
|
||||
- No other test or script names this file. It runs through `packages/conversation`'s `node --test tests/`.
|
||||
- No 64-hex value in the file.
|
||||
|
||||
Changed path `agents/researcher/work/queue-220/BUILD.md`: no pin.
|
||||
|
||||
`node --check` passed on both changed `.mjs` files.
|
||||
|
||||
## Phase B plan (not run)
|
||||
|
||||
The reader test uses `os.tmpdir()`. Set `TMPDIR` to a scratch dir first, per the rule against `/tmp`.
|
||||
|
||||
```
|
||||
export TMPDIR="$(mktemp -d /mnt/storage/scratch/tmp/r220XXXXXX)"
|
||||
cd packages/conversation && node --test tests/reader.test.mjs
|
||||
cd packages/conversation && node --test tests/
|
||||
cd packages/discord && node --test tests/
|
||||
cd packages/control-board && node --test tests/serve.test.mjs
|
||||
```
|
||||
|
||||
The first command is the touched file whole. The second and third are the conversation and discord suites whole, as the brief asks. The control-board serve test is not in the brief. It consumes `reader.mjs`, so I recommend it. Sage can drop it.
|
||||
|
||||
## Questions for Sage
|
||||
|
||||
1. **Business link (blocks M8 and the M7 wiring).** Neither rule can be built from this row's file list, because the inputs don't exist in the code:
|
||||
- A reader root carries `seat` (the spec's `agent`) and `project` (the repo directory name). It has no business id. Roots come from control-board scan specs (`packages/control-board/src/scan.mjs`, `discoverRepoAgents`) and seat registrations. Neither carries a definition.
|
||||
- A Discord binding has `seat` and no business key (`packages/discord/src/binding.mjs`, `TOP_KEYS`).
|
||||
- Creative status lives only in the business file: `roles.<instance>.definition` in `CREATIVE_DEFINITIONS` (`packages/business/src/vocabulary.mjs:176`). A library project is `projects.<id>.kind === "library"` (`packages/business/src/business.mjs:243`).
|
||||
- Creative thread sessions are stored at `<dataRoot>/threads/<business>/<thread>/session` (`packages/seat/src/session.mjs:59-64`), outside the `.pi/state` roots the board scans. M7 may therefore reach only creative seats that have a `.pi/state` session root. Please confirm.
|
||||
- The design does not define the mapping from a seat to a business.
|
||||
|
||||
Recommendation: add a required `business` key to the binding, validated against `businesses/<id>.json`. The board builds `isCreative` from `loadBusiness(id)`: a root is creative when its seat is a `roles` key whose definition is in `CREATIVE_DEFINITIONS`, and the business has a library project. This needs file scope for `packages/discord/src/binding.mjs` and `cli.mjs` (M8), and `packages/control-board/src/serve.mjs` (M7 wiring). None of those is on this row's list. I need that scope amendment and the mapping ruling before I build M8 or the wiring.
|
||||
|
||||
2. **M8 refusal code for a `mosaic-bus` binding with `tools`.** The design names `discord-engine-not-allowed` for the engine case and no code for the tools case. A new code goes to you first. My proposal is to reuse `discord-engine-not-allowed` with a message that names the tools key.
|
||||
|
||||
3. **`reconcile` on `conversation-not-listed`.** I chose `false`. Confirm or change.
|
||||
|
||||
4. **HTTP status for `conversation-not-listed`.** `REFUSAL_STATUS` in `packages/control-board/src/serve.mjs` has no entry, so the route returns 422. My proposal is 404, next to `unknown-conversation`. This needs that file on the scope list too.
|
||||
|
||||
Phase A stops here. On your "slot clear" I will run the phase B commands above. I will not build M8 or the wiring until question 1 is answered.
|
||||
@@ -1,481 +0,0 @@
|
||||
# Row 220 r2: phase A round 2 (M7, M8, business helper, REFUSAL_STATUS)
|
||||
|
||||
Phase A only. No node, npm, `node --check` or test run has happened in this round, and nothing is
|
||||
committed. Phase B (the suites) waits for the coordinator's "slot clear".
|
||||
|
||||
- Owner: researcher seat (Haiku 5.5 subagent), dispatched by Sage under lead decision 129.
|
||||
- Reviewer: Filbert.
|
||||
- Branch: `researcher/row220-r2`, from `origin/refactor` at da9555ca2. The r1 cherry-pick 3fa833715 is
|
||||
staged with `-n`.
|
||||
- Rulings: `agents/sage/work/queue-220/RULINGS.md` on origin/refactor (da9555ca2). Import-by-path is
|
||||
followed: no export line in `packages/business/src/index.mjs`, and `packages/discord/src/binding.mjs`
|
||||
is untouched.
|
||||
|
||||
## Coordinator rulings, applied
|
||||
|
||||
Round 1 answers (earlier packet): applied as before. Absent `businesses/` gives empty sets only on
|
||||
ENOENT; other failures throw. stack-copy test joins phase B. Webui fixtures pass `businessDir` (renamed `configDirectory` in round 2b below). Discord
|
||||
refusals exit 2.
|
||||
|
||||
Round 2 rulings (coordinator message, this round):
|
||||
|
||||
1. **Dangling symlink at `businesses/` refuses.** `loadSeatSets` calls `lstatSync(join(dir, "businesses"))`
|
||||
first. Only an lstat ENOENT (nothing at the path) returns empty sets. If something is there and
|
||||
`readdirSync` fails for any reason, including ENOENT through a dangling link, it throws `BusinessError`.
|
||||
Test added: "a businesses symlink to a missing target throws; no empty set". The absent-directory test is kept.
|
||||
2. **Sets are read once per public reader call.** `createReader` takes `creativeCheck`, a factory. Each
|
||||
`catalogue()`, `open()` and `next()` calls it once, then applies the returned predicate to every root.
|
||||
A load failure refuses `conversation-business-unreadable` for the whole call. `serve.mjs` exports
|
||||
`creativeCheck(dir = configDir())`, the factory, and `createServer` passes `creativeCheck(businessDir)`.
|
||||
Test added: "one catalogue calls the creative check once, however many roots it lists". It counts factory
|
||||
calls: 1 after `catalogue()`, 2 after one `open()`.
|
||||
3. **Phase B adds three test files:** `packages/webui/tests/tester-browser.test.mjs`,
|
||||
`tester-cli.test.mjs` and `proof-cli.test.mjs`, run whole (they reach `fixture.mjs` through
|
||||
`tester-fixture.mjs`). Phase B is 36 files: 33 plus these 3.
|
||||
4. **Scope additions approved** (coordinator round 2 ruling 4): `packages/control-board/tests/discord.test.mjs`
|
||||
line 123 and the five webui test files (`fixture.mjs`, `history-fixture.mjs`, `relaunch.test.mjs`,
|
||||
`return-flow.test.mjs`, `discord.test.mjs`). They stop tests reading the real config directory.
|
||||
5. **TMPDIR** for phase B is set by the coordinator under `/mnt/storage/scratch/tmp`. No code change.
|
||||
|
||||
## Discord exit code
|
||||
|
||||
Refusals from the library-seat check exit 2, the code the Discord CLI already uses for configuration and
|
||||
binding refusals.
|
||||
|
||||
- `packages/discord/src/errors.mjs` line 2: `// exitCode follows docs/TOOLS.md: 1 operation failed, 2 invalid data or configuration, 3 refused by a brake (STOP or a held binding; a supervisor must not retry), 4 usage.`
|
||||
- `packages/discord/src/errors.mjs` line 5: `constructor(message, exitCode = 2, details = undefined)`.
|
||||
- Config refusal precedent: `packages/discord/src/binding.mjs` line 67, `throw new DiscordError(\`config.dataRoot must be an absolute path: ${path}\`);`. No explicit code, so exit 2.
|
||||
- `checkLibraryEngine` in `packages/discord/src/cli.mjs` lines 115 to 122. Each refusal passes `, 2)`:
|
||||
`discord-business-unreadable` (115), `discord-engine-not-allowed` for a non-mosaic-bus engine (119),
|
||||
and `discord-engine-not-allowed` for a mosaic-bus binding that declares `tools` (122).
|
||||
- `grep -n ", 3)" packages/discord/src/cli.mjs` finds nothing. `grep -rn "status, 3" packages/discord/tests`
|
||||
finds only existing brake tests in recover.test.mjs and binding.test.mjs. Not changed.
|
||||
`library-seat.test.mjs` asserts 2 on all three refusals.
|
||||
|
||||
## Files (round 2, uncommitted)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/business/src/library-seats.mjs` | new: `loadSeatSets({ dir, rolesDir })` returns `{ librarySeats, creativeSeats }`; lstat-first (ruling 1); read per call |
|
||||
| `packages/business/tests/library-seats.test.mjs` | new: 7 tests (6 from r1 plus the dangling symlink) |
|
||||
| `packages/conversation/src/reader.mjs` | r1 plus r2: `NOT_LISTED`; `creativeCheck` factory option, called once per `catalogue()`, `open()` and `next()`; `catalogue()` wrapped so a refusal returns `{ ok: false }` |
|
||||
| `packages/conversation/tests/reader.test.mjs` | r1: 3 M7 tests, now on `creativeCheck`; r2: 1 spy test (one factory call per catalogue) |
|
||||
| `packages/control-board/src/serve.mjs` | `REFUSAL_STATUS`: `conversation-not-listed` 404, `conversation-business-unreadable` 503; `export creativeCheck(dir = configDir())` (factory); `createServer({ businessDir = configDir() })` wires `creativeCheck: creativeCheck(businessDir)` |
|
||||
| `packages/control-board/tests/creative-hidden.test.mjs` | new: 4 tests (3 from r1 plus known-and-unknown id both 503), switched to `creativeCheck` |
|
||||
| `packages/control-board/tests/serve.test.mjs` | pin test adds `NOT_LISTED` and `conversation-business-unreadable`; conversation fixture passes `businessDir` |
|
||||
| `packages/control-board/tests/discord.test.mjs` | line 123 passes `businessDir` (ruling 4) |
|
||||
| `packages/discord/src/cli.mjs` | `checkLibraryEngine` called from `prepare()`; exit 2 |
|
||||
| `packages/discord/tests/helpers.mjs` | `makeDeployment` creates an empty `businesses/` beside `config.json` |
|
||||
| `packages/discord/tests/library-seat.test.mjs` | new: 5 tests, all `--offline`, all asserting exit 2 |
|
||||
| `packages/webui/tests/fixture.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/history-fixture.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/relaunch.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/return-flow.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/discord.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
|
||||
Not touched: `packages/business/src/index.mjs`, `packages/discord/src/binding.mjs`, `scripts/stack-copy.sh`.
|
||||
|
||||
## Brief item to test map
|
||||
|
||||
| Brief item | Test |
|
||||
|---|---|
|
||||
| Q1 helper: library and creative sets from business files | `business/tests/library-seats.test.mjs` "library seats come from library businesses..." |
|
||||
| Q1 a non-library business puts no seat in either set | `library-seats.test.mjs` "a business with no library project..." |
|
||||
| Q1 absent businesses directory gives empty sets | `library-seats.test.mjs` "an absent businesses directory gives empty sets" |
|
||||
| Q1 any other failure throws, no empty set | `library-seats.test.mjs` "a businesses path that is not a directory throws"; "a businesses symlink to a missing target throws"; "a business file that can't be read throws"; "a business file that is not valid JSON throws" |
|
||||
| Round 2 ruling 2: one read per public call | `conversation/tests/reader.test.mjs` "one catalogue calls the creative check once..." |
|
||||
| M7 hidden from catalogue, open and next | `control-board/tests/creative-hidden.test.mjs` "the check hides the creative seat..."; r1 reader tests (catalogue, open, next, symlink and placeholder) |
|
||||
| M7 second wall: thread store | `creative-hidden.test.mjs` "a thread store is not a root from a spec..." (asserts `rootsFromSpecs` drops it, then a root over it is hidden with 404 `conversation-not-listed`) |
|
||||
| M7 unreadable business refuses listing and open | `creative-hidden.test.mjs` "an unreadable business file refuses the listing and open with 503" |
|
||||
| Coordinator follow-up: known and unknown id both 503 when unreadable | `creative-hidden.test.mjs` "with an unreadable business file, a known and an unknown id both refuse with 503" |
|
||||
| M8 non-mosaic-bus engine on a library seat | `discord/tests/library-seat.test.mjs` "a library seat's binding on a non-mosaic-bus engine is refused" (exit 2, message names the engine) |
|
||||
| M8 mosaic-bus binding that declares tools | `library-seat.test.mjs` "...declares tools is refused, and the message names tools" (exit 2) |
|
||||
| M8 mosaic-bus, no tools, allowed | `library-seat.test.mjs` "...with no tools passes the engine rule" |
|
||||
| M8 unreadable business file | `library-seat.test.mjs` "an unreadable business file refuses check with discord-business-unreadable" (exit 2) |
|
||||
| M8 seat in no library business checks clean | `library-seat.test.mjs` "a binding on a seat that is in no library business checks clean" |
|
||||
| Q3 reconcile false | r1 reader test asserts `[false, "conversation-not-listed", false]` |
|
||||
| Q4 HTTP 404 and 503 | `creative-hidden.test.mjs` (status asserted); `serve.test.mjs` pin test |
|
||||
|
||||
## Choices (for Filbert to check)
|
||||
|
||||
1. **Absent versus unreadable `businesses/`.** Only nothing at the path reads as empty (lstat ENOENT). A
|
||||
dangling symlink, a file, or an unreadable directory throws. Coordinator ruling 1.
|
||||
2. **Sets are read once per public call.** `creativeCheck()` runs once in each of `catalogue()`, `open()` and
|
||||
`next()`. The returned predicate answers every root in that call. A catalogue with zero roots still
|
||||
loads the sets once, so an unreadable business file refuses even an empty list. Coordinator ruling 2.
|
||||
3. **Behavior change, kept on coordinator ruling (round 2 follow-up): an unknown id or cursor loads the sets
|
||||
first.** In `open()`, the check runs after `checkCaller` and before `resolveConversation`. In `next()`, it
|
||||
runs at the start of the try. So with an unreadable business file, a known id and an unknown id both give
|
||||
503 `conversation-business-unreadable`, where round 1 gave 404 `unknown-conversation` for the unknown id.
|
||||
Checking the id first would give 503 for a hidden creative id and 404 for an unknown one, which reveals
|
||||
whether the hidden id exists. Loading first answers every id the same way (fail closed). This reverses
|
||||
round 1 choice 3. Test: `creative-hidden.test.mjs` "with an unreadable business file, a known and an unknown id both refuse with 503".
|
||||
4. **Discord refusals use the `code: message` form and exit 2.** Exit 3 is for brake refusals (STOP or a held
|
||||
binding). A seat's engine or library membership is not a brake. The business CLI is not used as precedent.
|
||||
5. **The thread-store wall exists in part already.** `rootsFromSpecs` (`reader.mjs` line 44) drops any
|
||||
sessions directory that is not `<project>/.pi/state/<seat>/sessions`. The M7 check is the second wall, for
|
||||
roots built another way. The test checks both.
|
||||
|
||||
## Static checks (read-only; no node)
|
||||
|
||||
- **COPY_PATHS** (`scripts/stack-copy.sh` 29 to 41): `packages/business/src`, `packages/conversation/src`,
|
||||
`packages/discord/src` and `packages/library/src` are present. `packages/control-board/src` is not. COPY_PATHS
|
||||
is not changed.
|
||||
- **Stale names.** `grep -rn -E "isCreative|creativeRootPredicate" packages scripts` finds nothing after this round's edits.
|
||||
- **Board starts without `businessDir`.** Webui: all five direct starts pass it. Control board:
|
||||
- `serve.test.mjs` has seven single-line starts without it (lines 402, 445, 464, 483, 509, 528, 907) and four
|
||||
multi-line starts without it (114, 149, 229, 258). None of those tests reaches a conversation route.
|
||||
Every conversation route call in the file (lines 1027 to 1171) follows a start that passes `businessDir`
|
||||
(1006, 1075, 1124). Checked by line order.
|
||||
- `creative-hidden.test.mjs` builds `creativeCheck(f.dir)` on its own scratch directory. No default is used.
|
||||
- `discord.test.mjs` passes `businessDir` (line 123).
|
||||
- **Consumers of the two fixtures** (exact import strings, `from './fixture.mjs'`, `from './history-fixture.mjs'`):
|
||||
- Direct `fixture.mjs` importers, 31 test files: board-held, browser-edge, browser, bus-browser, bus,
|
||||
chat-browser, chat, discord, docs-browser, editor-browser, editor, instances-browser, limits-browser,
|
||||
limits-route, messages, preview, proof, relaunch, return-flow, routes-browser, send-browser, send, serve,
|
||||
settings-form, shell, sso-browser, sso, tester-listener, usage-browser, usage, views.
|
||||
- `history-fixture.mjs` importers, 2 test files: conversation, history-return-flow.
|
||||
- Total fixture consumers: 33. Plus `tester-browser`, `tester-cli` and `proof-cli` (through
|
||||
`tester-fixture.mjs`, ruling 3): 36 files in phase B.
|
||||
|
||||
## Phase B commands (after "slot clear" only)
|
||||
|
||||
TMPDIR is set by the coordinator under `/mnt/storage/scratch/tmp`. The commands below assume it. The new
|
||||
tests call `tmpdir()`, so an unset TMPDIR would put them under `/tmp`, which bwrap hides.
|
||||
|
||||
```sh
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/conversation && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/discord && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/business && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/control-board && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e && scripts/test-discord.sh
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/cli && node --test tests/stack-copy.test.mjs
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/webui && node --test tests/board-held.test.mjs tests/browser-edge.test.mjs tests/browser.test.mjs tests/bus-browser.test.mjs tests/bus.test.mjs tests/chat-browser.test.mjs tests/chat.test.mjs tests/conversation.test.mjs tests/discord.test.mjs tests/docs-browser.test.mjs tests/editor-browser.test.mjs tests/editor.test.mjs tests/history-return-flow.test.mjs tests/instances-browser.test.mjs tests/limits-browser.test.mjs tests/limits-route.test.mjs tests/messages.test.mjs tests/preview.test.mjs tests/proof.test.mjs tests/relaunch.test.mjs tests/return-flow.test.mjs tests/routes-browser.test.mjs tests/send-browser.test.mjs tests/send.test.mjs tests/serve.test.mjs tests/settings-form.test.mjs tests/shell.test.mjs tests/sso-browser.test.mjs tests/sso.test.mjs tests/tester-listener.test.mjs tests/usage-browser.test.mjs tests/usage.test.mjs tests/views.test.mjs tests/tester-browser.test.mjs tests/tester-cli.test.mjs tests/proof-cli.test.mjs
|
||||
```
|
||||
|
||||
The webui line is 36 files, run whole. `history-fixture.mjs` and `tester-fixture.mjs` run through their
|
||||
consumers. No test has been run. The expectations above come from reading the code.
|
||||
|
||||
## Open items
|
||||
|
||||
a. Phase B checks the dangling-symlink test on this platform (lstat then readdir ENOENT must throw).
|
||||
b. Phase B checks the spy count (1 after catalogue, 2 after one open). A failure here means a public call
|
||||
reads the factory more than once.
|
||||
c. Choice 3 is settled by the coordinator: keep 503 for unknown ids too. Phase B checks the new known-and-unknown test.
|
||||
d. The browser-based webui tests need a browser in this environment. A failure there would be environmental.
|
||||
Phase B will show which.
|
||||
e. Nothing here has been executed. Phase B checks every expectation, including the unreadable-file test
|
||||
(EACCES must throw), the root skip (uid 0 skips), and the webui consumers.
|
||||
|
||||
## Resolved
|
||||
|
||||
- Dangling symlink at `businesses/` (round 1 open item a, and round 1 question 1): refuses, ruling 1.
|
||||
- Tester and proof CLI tests (round 1 question 2): join phase B, ruling 3.
|
||||
- Per-root reads (round 1 open item b): replaced by one read per public call, ruling 2.
|
||||
|
||||
## Rebase onto aca16404c, run limits, index export (2026-10-22, sage-2708)
|
||||
|
||||
Added after the text above; nothing above is edited. "Nothing is committed" above was true when written.
|
||||
|
||||
- Commit `6f656abac` on `researcher/row220-r2`, one commit, rebased from da9555ca2 onto origin/refactor
|
||||
`aca16404c`. Row 253 has not landed; this rebases again onto 253's tip before any gate.
|
||||
- Drift check, da9555ca2..aca16404c against my 18 paths: one overlap, `packages/discord/src/cli.mjs`
|
||||
(the processing-control imports, rows 264 and neighbours). The conflict was the import block only. I
|
||||
kept both sides: the two landed `processing-control` imports and my `loadSeatSets` import. No landed hunk
|
||||
is reverted. `checkLibraryEngine` and its call in `prepare()` merged without a marker.
|
||||
- Board starts: no test added since da9555ca2 starts the control board or reads a conversation route
|
||||
without `businessDir`. `packages/webui/tests/shell.test.mjs` fakes the board and does not start it.
|
||||
- Two edits since the r2 text: (1) a missing space in `reader.mjs` `next()` (`held =typeof`) fixed;
|
||||
(2) `packages/business/src/index.mjs` gets `export { loadSeatSets } from "./library-seats.mjs";`. The text
|
||||
above says "no export line". RULINGS.md Q1 says the helper is "exported from the package index" and the
|
||||
scope amendment lists "its export line", so the file wins and the export is added. The two callers still
|
||||
import by path, so nothing needs the export today. Filbert: drop the line if you read the ruling otherwise.
|
||||
The export adds no test; `node --check` has not been run on it (no node before the CLEAR).
|
||||
|
||||
### Run limits (fixed before any run)
|
||||
|
||||
Every node file runs as its own `setsid -w timeout -k 10 LIMIT node --test FILE`; nothing runs unbounded.
|
||||
|
||||
- Node test files: LIMIT 900 s. I found no recorded per-file time for the conversation, discord, business,
|
||||
control-board or webui files. 900 s is above the longest recorded whole-package run I know of (cli, 591 s,
|
||||
row 281 BUILD.md "Runs" step 3), so it holds the 3x rule for any single file. The first run records each
|
||||
file's time; if a file passes 300 s, the limit is re-derived from it before the next run.
|
||||
- Shell scripts: `timeout -k 10 LIMIT bash scripts/test-X.sh`, LIMIT = 3x the longest recorded run plus
|
||||
120 s, taken from Megavolt's row 281 table (`agents/megavolt/work/queue-281/BUILD.md`, from Gander's
|
||||
`agents/gander/work/queue-278/SHELL-LIMITS.json`). A script with no limit does not run.
|
||||
|
||||
| Script | LIMIT | Script | LIMIT |
|
||||
|---|---|---|---|
|
||||
| test-auth | 126 s | test-land-work | 158 s |
|
||||
| test-conductor | 145 s | test-queue | 191 s |
|
||||
| test-config | 130 s | test-release | 157 s |
|
||||
| test-discord | 147 s | test-task | 231 s |
|
||||
| test-extension-package | 130 s | test-foundation | 240 s |
|
||||
|
||||
Any timeout goes to Sage, then Mos, unplaced (mos-1275).
|
||||
|
||||
## Round 2b: Filbert's early look and sage-2719 (2026-10-22T14:46Z)
|
||||
|
||||
Added after the text above; nothing above is edited. Where earlier text says `businessDir`, the parameter is
|
||||
now `configDirectory` (below). Second commit on `researcher/row220-r2`, on top of ea6dc9eff. Not pushed, not
|
||||
run; `node --check` has not been run on any of these files (no node before the CLEAR).
|
||||
|
||||
Rulings applied (sage-2719): Q1 keep the index export. C1, C2 and the fixture scope are changes.
|
||||
|
||||
### Fixture scope: the parameter is a config directory
|
||||
|
||||
`createServer` took `businessDir`, but `creativeCheck(dir)` reads `<dir>/businesses`, so every fixture that
|
||||
passed `join(root, 'businesses')` made the reader look in `<root>/businesses/businesses`. That read nothing
|
||||
(ENOENT, empty sets): isolation from the real config held by accident. Now:
|
||||
|
||||
- `createServer({ configDirectory = configDir() })` in `serve.mjs`. The parameter is not named `configDir`
|
||||
because the import of that name is the default's source.
|
||||
- Every fixture passes the config directory it owns: `configDirectory: root` in the five webui files,
|
||||
`f.root` in `control-board/tests/discord.test.mjs`, and `f.configDirectory` in `serve.test.mjs`.
|
||||
`conversationFixture()` and `replyFixture()` both return `configDirectory: root`. The seven reply starts
|
||||
that passed `f.businessDir` (undefined, falling back to the real `configDir()`) now pass a real value.
|
||||
- The dead `mkdirSync(join(root, 'businesses'))` lines in the five webui fixtures and in `serve.test.mjs`
|
||||
are removed. So is the one in `discord/tests/helpers.mjs` `makeDeployment`, whose comment said "a missing
|
||||
one refuses"; that was false (lstat ENOENT gives empty sets). The comment now says nothing there means no
|
||||
library seat. Tests that need a `businesses/` directory create it: `writeJson` does, and the two
|
||||
unreadable-file tests in `library-seat.test.mjs` call `mkdirSync` themselves.
|
||||
- Proof that the fixture's own business file is the one read: the new default-reader test (C2) hides `bean`
|
||||
and shows `pm`, and nothing else in that fixture says which is which.
|
||||
|
||||
### C1: M8 on `run`
|
||||
|
||||
`library-seat.test.mjs` gains three `run` tests: a raw engine, a mosaic-bus binding with `tools`, and an
|
||||
unreadable business file. Each asserts exit 2 and the code in stderr, and that `s.d.journalDir` does not
|
||||
exist (`ensureJournal` comes after `prepare()` in `run`, so nothing was started). `run()` calls
|
||||
`initializeControl(dataRoot)` first, because `run` stops at `assertProcessing` before `prepare()`, as the
|
||||
STOP test in `binding.test.mjs:203` does. There is no `run` test for a clean seat: that would go on to the
|
||||
network.
|
||||
|
||||
### C2: the default reader
|
||||
|
||||
`creative-hidden.test.mjs`, "the board's default reader hides a creative seat's session from the listing and
|
||||
the detail route". It calls `startServer` with no `conversationReader`, `configDirectory: f.dir`, a `bean`
|
||||
session and a `pm` session on disk, and drives HTTP: the listing holds `pm` only; bean's detail is 404
|
||||
`conversation-not-listed`; pm's detail is 200; after a broken business file lands the listing is 503
|
||||
`conversation-business-unreadable`.
|
||||
|
||||
### Mutants to throw (CLEAR only; none thrown; the kill claims are unverified)
|
||||
|
||||
| Id | Mutation | Meant to be killed by |
|
||||
|---|---|---|
|
||||
| M7-w | delete `creativeCheck: creativeCheck(configDirectory),` in `createServer` (`serve.mjs`) | the default-reader test, first assertion (the listing shows `bean`) |
|
||||
| M7-c | `hiddenCheck()` in `reader.mjs` open() returns `() => false` | reader.test "hides the creative seat" and the `creative-hidden` 404 asserts |
|
||||
| M7-o | `creativeCheck` catch returns an empty set instead of throwing | the 503 asserts in `creative-hidden` |
|
||||
| M8-p | delete `checkLibraryEngine(...)` in `prepare()` | the three `check` refusals and the three `run` refusals; one mutant covers both verbs because they share `prepare()` |
|
||||
| M8-e | `loadSeatSets` returns empty sets on any lstat error | `library-seats` ENOENT-vs-file tests |
|
||||
|
||||
### `refusalResponse` and `catalogue()`'s refusal (past "predicate wiring and REFUSAL_STATUS only")
|
||||
|
||||
Why it is needed: the reader's `catalogue()` now returns a refusal when the business check throws (M7: no
|
||||
partial list), and `conversationResponse` had no way to turn a catalogue refusal into a status. `open` and
|
||||
`next` already built `{ status: REFUSAL_STATUS[code] ?? 422, body: { error, refusal: { code, reconcile } } }`
|
||||
inline; that expression moved, character for character, into `refusalResponse(out)` so the listing could
|
||||
use it too.
|
||||
|
||||
No existing route's status or body changes:
|
||||
|
||||
- `open` and `next` refusals: the same expression, now behind a function. `git diff aca16404c..HEAD --
|
||||
packages/control-board/src/serve.mjs` shows the line as a move.
|
||||
- `/api/conversations` success: `{ status: 200, body: listing }` where `listing` is what the route returned
|
||||
before. `catalogue()` returned `ok: true` on every path before this row, so the new branch is reached only
|
||||
by the business refusal.
|
||||
- The "no parameters are accepted" 400 is untouched.
|
||||
|
||||
Phase B runs `serve.test.mjs`, `discord.test.mjs` and the conversation route tests whole, so Filbert can
|
||||
check this statement against a green run.
|
||||
|
||||
### Smaller changes from the early look
|
||||
|
||||
- Spy test: one `next()` call adds a third `creativeCheck` count (`reader.test.mjs`). The catalogue assert
|
||||
also checks `refusedRoots` is empty, in the same test and in the thread-store test in `creative-hidden`.
|
||||
- `library-seats.test.mjs` mode-000 test pins `BusinessError` and its message (`must be a regular,
|
||||
non-symbolic-link file`, from `readJsonFile`'s open failure).
|
||||
- "passes the engine rule" now asserts exit 0 and `offline check passed`. The expectation is unverified: if
|
||||
a mosaic-bus binding fails some other offline check, that is a first-run red to place (mos-1272).
|
||||
|
||||
### Carried, not changed here
|
||||
|
||||
- `conversation/src/controller.mjs:176` builds a reader with no `creativeCheck`. No `src` file imports it
|
||||
today; a live wiring must pass the check. Raised for Sage.
|
||||
- `discord run` reads the business file once at start. `engine` and `tools` are FIXED_KEYS, so only a
|
||||
business-file change while a connector runs goes unseen until the next start.
|
||||
- `creative-hidden` and `library-seats` use the repo `roles/` as the default `rolesDir`.
|
||||
|
||||
### Gate additions
|
||||
|
||||
Phase B list is unchanged: the same suites run whole, plus the files above are inside them. The drift check
|
||||
against `aca16404c..refactor`, including files these tests copy or read, repeats after the rebase onto 253.
|
||||
|
||||
## Round 2c: refusalResponse statement corrected from the diff, controller.mjs follow-up (2026-10-22T14:55Z, sage-2729)
|
||||
|
||||
Appended after Round 2b; nothing above is edited. This replaces Round 2b's refusalResponse statement, which I
|
||||
wrote from memory of the code. Source: `git diff aca16404c HEAD -- packages/control-board/src/serve.mjs` and
|
||||
`packages/conversation/src/reader.mjs`, read in full. No test has run.
|
||||
|
||||
### What the diff shows in serve.mjs
|
||||
|
||||
- `open` and `next` refusal: the object expression `{ status: REFUSAL_STATUS[out.refusal.code] ?? 422, body: {
|
||||
error: out.refusal.message, refusal: { code, reconcile } } }` moved unchanged into `refusalResponse(out)`.
|
||||
The two call sites reach it by `return refusalResponse(out)`. Status and body are identical for every code
|
||||
that reached it before.
|
||||
- `REFUSAL_STATUS` gains two keys, `conversation-not-listed` (404) and `conversation-business-unreadable`
|
||||
(503). Before, those codes did not exist, so no earlier response used them or changes. Every other key is
|
||||
untouched.
|
||||
- `/api/conversations` with a query string: the 400 `no parameters are accepted` line is untouched.
|
||||
- `/api/conversations` success: before, `{ status: 200, body: reader.catalogue() }`; now the same object
|
||||
when `listing.ok` (it is the same `listing`), so the 200 body is unchanged.
|
||||
- The one new response shape: `/api/conversations` can return a refusal (503 or the refusal's status) when
|
||||
the business check throws. That is the M7 behaviour, not a change to an existing route's response.
|
||||
|
||||
### What the diff shows in reader.mjs, past my Round 2b statement
|
||||
|
||||
`catalogue()` is now wrapped in `try { ... } catch (err) { return refuse(err); }`. `refuse` returns a refusal
|
||||
object for a `Refusal` and rethrows anything else (`reader.mjs:352-355`). So a non-Refusal error still
|
||||
propagates as before. A `Refusal` escaping the loop body would now come back as `{ ok: false }` where it
|
||||
used to throw. The loop already catches `Refusal` from `listSessionFiles` and from `summarize`, so the only
|
||||
`Refusal` that newly reaches the wrapper is the hidden check's. I have not confirmed that `listRoots()`
|
||||
(`rootsFromSpecs`, `loadRegistrations`) never throws a `Refusal`; if it does, that call now returns a
|
||||
refusal. Filbert's round check should read `rootsFromSpecs` for `Refusal` throws.
|
||||
|
||||
Round 2b said the old `catalogue()` "returned ok: true on every path". True for returns; it could throw.
|
||||
|
||||
### Named follow-up for DEFERRED (Sage lands it with 220; it does not block 220)
|
||||
|
||||
`packages/conversation/src/controller.mjs:176` builds a reader with no `creativeCheck`. No `src` file imports
|
||||
`controller.mjs` today, so no live path leaks. Whoever first wires `controller.mjs` live must pass
|
||||
`creativeCheck` (`creativeCheck(configDirectory)` from `control-board/src/serve.mjs`, or the reader's own
|
||||
equivalent) and add a test that a creative seat's sessions stay hidden through that path.
|
||||
|
||||
## Round 2d: can listRoots() throw a Refusal into catalogue()? No (2026-10-22T14:53Z, sage-2730)
|
||||
|
||||
Appended after Round 2c; closes the question Round 2c left open. Static read on 9a9f6dd54; nothing run.
|
||||
Filbert reached the same answer independently (filbert-220-r2c-static).
|
||||
|
||||
`catalogue()` calls `listRoots()` inside the new `try`. In production that is `serve.mjs:261`,
|
||||
`roots: () => rootsFromSpecs(specs, loadRegistrations(seatsDir).registrations)`. The only `Refusal` class
|
||||
the reader catches is the one in `conversation/src/safe-fs.mjs:21`.
|
||||
|
||||
- `rootsFromSpecs` (`conversation/src/reader.mjs:38-71`) calls `resolve`, `join`, `basename`, `Array.find`,
|
||||
`toTime` and `samePath`, and constructs no `Refusal`.
|
||||
- `toTime` (`conversation/src/pi.mjs:21-26`) returns a value or null and has no throw.
|
||||
- `samePath` (`seat/src/seat.mjs:261-269`) wraps `realpathSync` in try/catch and returns false.
|
||||
- `loadRegistrations` (`control-board/src/scan.mjs:272-292`) catches `SeatError` and rethrows anything
|
||||
else. It calls `readRegistration` (`seat/src/seat.mjs:200-216`), which throws `SeatError` only.
|
||||
- `grep -n "Refusal\|refuse(" packages/control-board/src/scan.mjs packages/seat/src/seat.mjs` finds
|
||||
nothing, and neither file imports `safe-fs.mjs` (checked with `grep -n "^import"`). A `Refusal` cannot
|
||||
originate in either.
|
||||
- Inside the loop, `listSessionFiles` is caught at `reader.mjs` (the `catch (err)` that rethrows
|
||||
non-Refusals) and `summarize` turns an `openSessionFile` Refusal into a `denied` row.
|
||||
|
||||
So the only `Refusal` that reaches the new catch is the hidden check's `conversation-business-unreadable`.
|
||||
Any other error still throws through `refuse()` (`reader.mjs:352-355`) exactly as before.
|
||||
|
||||
Therefore `/api/conversations` has no status change for a path that existed before this row: the old
|
||||
throw paths are unreachable by a `Refusal`. The one new response is the business refusal, already pinned by
|
||||
the creative-hidden 503 assertion and the reader catalogue-refusal test. No extra test is added.
|
||||
|
||||
The exception is an injected `roots` function that throws a `Refusal` itself; only a test could pass one,
|
||||
and none does.
|
||||
|
||||
Time correction (2026-10-22T14:51:01Z): the heading times of Round 2c (14:55Z) and Round 2d (14:53Z) were written before I read the clock. The commits were made at 14:49:40Z (9a9f6dd54) and 14:50:52Z (176ac262e). The heading of Round 2b (14:46Z) is also an estimate; its commit e4a204836 was made before 14:47:53Z.
|
||||
|
||||
## Round 4: rebase onto 45dca69f6, drift check (2026-10-23T18:53:43Z, sage-3576)
|
||||
|
||||
Appended; nothing above is edited. Git only; nothing run, nothing pushed. Branch researcher/row220-r4 holds the
|
||||
five row 220 commits on 45dca69f6 (origin/refactor, 10-23). Earlier base: 48e450ce8 (row 253).
|
||||
|
||||
- Rebase: clean. `git range-diff 48e450ce8..3ef6da90d 45dca69f6..researcher/row220-r4` shows all five commits as `=`,
|
||||
so no patch changed.
|
||||
- Landed in 48e450ce8..45dca69f6: 1175 files (47 packages/bus, 32 webui, 29 cli, 10 plugins, 9 seat, 4 harness,
|
||||
2 tasks, 7 scripts, BUILD-LOG.md, the rest agents/ and docs/).
|
||||
- Path overlap with this row's 19 paths: none.
|
||||
- Landed files in the packages these tests touch: webui (src/serve.mjs, src/cli.mjs, chat and bus files,
|
||||
tests). Row 220's webui test changes are five fixtures that start the control-board server with
|
||||
`configDirectory: root` and start the webui server with only `{ port, board }`; no landed webui file passes or reads
|
||||
configDirectory. The landed webui tests are not in this row's change set, but a whole webui run covers them.
|
||||
No landed file in control-board, conversation, discord or business.
|
||||
- Other users of the touched symbols (git grep on the r4 tree for createReader, rootsFromSpecs, creativeCheck,
|
||||
configDirectory, businessDir, loadSeatSets, checkLibraryEngine, conversationReader outside this diff):
|
||||
only conversation/src/controller.mjs:173-176 (the follow-up named in Round 2c) and the conversation README.
|
||||
- Hash pins: grep for 64-hex in packages/*/tests and scripts finds 364 files (positive control), and none pins a
|
||||
row 220 source file by hash. Path-string hits for packages/discord/src/cli.mjs (copy-service.test.mjs,
|
||||
scripts/test-discord.sh, discord-service.sh) are paths, not hashes.
|
||||
- Added to the gate list: `packages/discord/tests/helpers.mjs` is imported by 19 discord test files, and this row
|
||||
removed its empty `businesses` mkdir. Phase B must run the discord package whole (and `scripts/test-discord.sh`),
|
||||
not only library-seat.test.mjs.
|
||||
|
||||
## Round 5: phase B on 2bb146b28, R1 and R2 fixes (mos-1528) (2026-10-23T20:21:39Z, sage-3712)
|
||||
|
||||
Appended; nothing above is edited. The tree is 2bb146b28 plus the commits of branch researcher/row220-r5.
|
||||
|
||||
Phase B receipts on 2bb146b28 (researcher-r220-kit out-b1, out-b2, out-b3, out-mut, out-npm):
|
||||
|
||||
- discord 20 files, 200 pass; conversation 7 files, 187 pass; scripts/test-discord.sh rc=0, 82 passed.
|
||||
- control-board 7 files, 128 pass, 1 fail (serve.test.mjs, R2 below); business 26 files, 237 pass, 1 fail
|
||||
(library-seats.test.mjs, R1 below).
|
||||
- webui: first run 409 pass, 1 fail (editor-provenance.test.mjs). Cause: the esbuild in the worktree's
|
||||
node_modules resolved to the root copy (0.28.1) and not to packages/webui's own pin. Fix (R3, no repo change):
|
||||
`cd packages/webui && npm ci --ignore-scripts --no-audit --no-fund` in the worktree, output in out-npm/npm-ci.txt;
|
||||
esbuild is then 0.28.2. Rerun of all 65 webui files whole with MOSAIC_REQUIRE_EDITOR_BUILD=1: 65 of 65 rc=0, 410
|
||||
pass, 0 fail.
|
||||
|
||||
Fixes (both are test-only; src/ is unchanged from 2bb146b28; Mos ruled both in mos-1528):
|
||||
|
||||
- R1, `packages/business/tests/library-seats.test.mjs`, test "M7/M8: a business file that can't be read throws; no
|
||||
empty set". The fixture registers `rmSync(root)` as a `t.after` hook before the test registers its
|
||||
`chmodSync(file, 0o600)` restore, so on the way out the restore found the file already removed and threw ENOENT.
|
||||
Old: `t.after(() => chmodSync(file, 0o600));`. New: the same call inside try/catch that rethrows anything but
|
||||
ENOENT. The assertion is unchanged.
|
||||
- R2 (option A, mos-1528), `packages/control-board/tests/serve.test.mjs:1073`, test "conversation routes (F16): a
|
||||
foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers".
|
||||
Failure: the same-origin request at the end reached the route, the spy's `catalogue` returned the number
|
||||
`calls.push(...)` gives (1), and 220's `conversationResponse` reads `listing.ok`, so `refusalResponse(1)` threw on
|
||||
`out.refusal.code` and the route answered 500. Old text:
|
||||
`const spy = { catalogue: () => calls.push("catalogue"), open: () => calls.push("open"), next: () => calls.push("next") };`
|
||||
New text: `catalogue` records the call, then returns `{ ok: true, conversations: [], refusedRoots: [] }`; `open` and
|
||||
`next` are unchanged (the test never reaches them with a passing request). The assertions are unchanged: 200 for the
|
||||
same-origin request and `calls` equal to `["catalogue"]`. serve.mjs stays as 220 has it.
|
||||
Added test, same file before "every refusal code the reader can raise has an HTTP status": a reader whose
|
||||
`catalogue()` returns `{ ok: false, refusal }` gets `conversation-business-unreadable`'s status (503) and the body
|
||||
`{ error, refusal: { code, reconcile } }`. No such route test existed; the earlier claim that no route's status or
|
||||
body changed was wrong for a reader that returns a bare value, which the old spy did.
|
||||
|
||||
Mutants thrown on 2bb146b28 (out-mut; the base run for library-seats was red from R1, so a kill by that file is not
|
||||
shown there):
|
||||
|
||||
| Mutant | Result |
|
||||
| --- | --- |
|
||||
| M7-w | killed by control-board creative-hidden ("the board's default reader hides a creative seat's session…") |
|
||||
| M7-c | killed: creative-hidden 4 fail, conversation reader 3 fail |
|
||||
| M7-o | killed: creative-hidden 4 fail |
|
||||
| M8-p | killed: discord library-seat 6 fail |
|
||||
| M8-e | SURVIVED: no test failed other than the base red in library-seats; handled in the next entry |
|
||||
|
||||
Run limit: stack-release.test.mjs took 644 s. The per-file limit is 3 x 644 = 1932 s. Sage's gates run cli whole
|
||||
under 2100 s.
|
||||
|
||||
Reruns owed after r5 (run on Sage's CLEAR): the touched files whole (library-seats, serve, and the M8-e test),
|
||||
control-board whole, business whole, and every 220 mutant, M8-e included. Results are appended as a new entry.
|
||||
|
||||
## Round 5b: correction of the M8-e kill claim, and the test that kills it (2026-10-23T20:21:52Z, sage-3715)
|
||||
|
||||
Appended; nothing above is edited. This is a correction to the "Mutants to throw" table in this file (the M8-e
|
||||
row, around line 270), which says M8-e, "`loadSeatSets` returns empty sets on any lstat error", is killed by the
|
||||
`library-seats` ENOENT-vs-file tests. That claim is wrong. M8-e changes the lstat catch to
|
||||
`return { librarySeats: new Set(), creativeSeats: new Set() };` for every error code, so only a test that makes lstat
|
||||
fail with a code other than ENOENT can kill it. The file-as-businesses test and the dangling-symlink test I named
|
||||
make lstat succeed or fail with ENOENT, never another code, so they pass under M8-e. Phase B on 2bb146b28 showed
|
||||
the mutant surviving (out-mut, M8-e: no failure besides the base red).
|
||||
|
||||
Added in this commit, `packages/business/tests/library-seats.test.mjs`: "M7/M8: a config directory under a regular
|
||||
file throws (lstat fails with ENOTDIR); no empty set". The config path is a regular file, so
|
||||
`lstat(<file>/businesses)` fails with ENOTDIR; the test expects `BusinessError` matching "cannot list businesses".
|
||||
It is a new test in this row's own file, in its own commit, and droppable without touching R1 or R2.
|
||||
|
||||
The rerun of M8-e on r5 (the test must fail under it) is owed and runs on CLEAR; the result is appended as a new
|
||||
entry.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Violet
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 297 (the GitHub grant method, handed over from Bushroot).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 6229eb89) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `violet/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
+2
-4
@@ -1593,7 +1593,5 @@ 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-23T21:08:32Z | Filbert (048f3c63) | Rows 220 and 216 round 2 verdicts, r5 static deltas, replay drift check | 220 APPROVE 739b868b6 (comment 31383 #1721, rev 3696), 216 APPROVE dae76ccc2 (comment 31384 #1717, rev 3697) on Researcher's r5 reruns and mutants and Sage's joint gate GREEN on fbe57a05b (sage-3748); static r5 deltas clean; 216 N3 (get returns a universe's own supporting files on 283's base) binds C2-05c; no tests run by me
|
||||
2026-10-23 | Gladstone (433058fc-9cea-46e3-8554-6ddefb312762, Codex gpt-6.1-sol) | #1816 / Vikunja 614, mos-1531 | Built hostcheck octal-mode fix and two bare-scratch bundle verifies on gladstone/hostcheck-item5 from 93694c518. Three append-only test blocks; Node/tests held by Sage-3738, untested push authorized sage-3740. Queue number pending; Morgana static review, Sage gate/landing.
|
||||
2026-10-23T21:58:38Z | Sage (1ef1e4f8) | Rows 220, 216, 320 landings and queue-commit (mos-1528, mos-1532/1534/1536/1537) | 220+216 joint gate GREEN 21:05:34Z on fbe57a05b, pushed with Filbert's SESSIONS line 582b86935; #1721 and #1717 closed. Row 320 (#1816, Gladstone F1-F6): faf55618e replayed onto 45e3ca7bd as 094124b4e (code patch-id equal, log appends in one final commit), Morgana APPROVE r2 c31403, full gate GREEN 21:53:00Z 0 fail, pushed 21:53Z, done rev 3717, #1816 closed c31413, Vikunja 614 done. queue-commit 604523b20 holds revs 3678-3717. Mos sent N=094124b4e to ops-01 (mos-1540).
|
||||
2026-10-23T22:06Z | Duckworth (Opus 5.5) | row 239 (#1745) round 1 on r4 e0cb1db2c (honker-0206, rev 3719) | APPROVE (static): r4 range-diff all =, 16 paths byte-equal to r3, e0cb1db2c BUILD.md only, 4 channel-proof pins match; record waits for Sage's comment id and gap (duckworth-0295) |
|
||||
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.
|
||||
@@ -40,8 +40,8 @@ install -d -o root -g root -m 0755 /etc/mosaic-stack/node-keys /opt/mosaic-stack
|
||||
cd "$(mktemp -d)"
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/node-<v>-linux-x64.tar.xz
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt.sig
|
||||
gpg --no-default-keyring --keyring /etc/mosaic-stack/node-keys/pubring.kbx --verify SHASUMS256.txt.sig SHASUMS256.txt
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt.asc
|
||||
gpg --no-default-keyring --keyring /etc/mosaic-stack/node-keys/pubring.kbx --verify SHASUMS256.txt.asc SHASUMS256.txt
|
||||
grep " node-<v>-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c -
|
||||
echo "<pinned sha256> node-<v>-linux-x64.tar.xz" | sha256sum -c -
|
||||
tar -xJf node-<v>-linux-x64.tar.xz --no-same-owner
|
||||
@@ -189,12 +189,7 @@ First install:
|
||||
```sh
|
||||
# Copy the bundle in, then check it before anything reads it.
|
||||
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
|
||||
(
|
||||
bundle_check="$(mktemp -d)" || exit 1
|
||||
trap 'rm -rf "$bundle_check"' EXIT
|
||||
git init -q --bare "$bundle_check" &&
|
||||
git -C "$bundle_check" bundle verify /root/<sha>.bundle
|
||||
) || echo "STOP: bundle verify failed; do not continue"
|
||||
git bundle verify /root/<sha>.bundle
|
||||
|
||||
# Root's checkout. stack-upgrade.sh and case2-unit.sh run from here, never from a copy.
|
||||
git clone -q -b refactor /root/<sha>.bundle /root/mosaic-stack
|
||||
@@ -207,29 +202,11 @@ install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha
|
||||
sudo -u mosaic -H git clone -q -b refactor /srv/mosaic/incoming/<sha>.bundle /srv/mosaic/src/mosaic-stack
|
||||
```
|
||||
|
||||
After a first install, once both units are running (section 10), adopt the installed copy before
|
||||
any activate or upgrade. This step runs only on Mos's go, with Jason or Mos running it
|
||||
(`scripts/stack-release.sh:14`):
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H env PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
/srv/mosaic/src/mosaic-stack/scripts/stack-release.sh adopt <business> --instance dev --units external
|
||||
```
|
||||
|
||||
Each upgrade, before `stack-upgrade.sh upgrade`:
|
||||
|
||||
Only the first upgrade after adopt takes `--no-rehearsal`: append it to the upgrade command below.
|
||||
Adopt writes no backup, so omitting the flag then refuses `no-backup`. Subsequent upgrades omit it;
|
||||
when a backup exists, supplying it refuses `rehearsal-required`.
|
||||
|
||||
```sh
|
||||
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
|
||||
(
|
||||
bundle_check="$(mktemp -d)" || exit 1
|
||||
trap 'rm -rf "$bundle_check"' EXIT
|
||||
git init -q --bare "$bundle_check" &&
|
||||
git -C "$bundle_check" bundle verify /root/<sha>.bundle
|
||||
) || echo "STOP: bundle verify failed; do not continue"
|
||||
git bundle verify /root/<sha>.bundle
|
||||
git -C /root/mosaic-stack fetch -q /root/<sha>.bundle refs/heads/refactor:refs/remotes/origin/refactor
|
||||
git -C /root/mosaic-stack checkout -q --detach <sha>
|
||||
install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha>.bundle
|
||||
@@ -254,37 +231,13 @@ When a run doesn't exit 0, send Mos the exit and the last lines of `/var/log/mos
|
||||
the old copy. The next run refuses `args-differ`, and a render from that file would put the old copy back.
|
||||
Move the file the result line names into place with `mv -f`. The two differ only in the `--copy` value.
|
||||
|
||||
After `case2-install.sh` succeeds, create the human CLI wrapper before running
|
||||
`stack-copy.sh check --units external`. Creating this wrapper is a separate live
|
||||
step requiring Mos's go (`scripts/stack-copy.sh:3`); the install does not create it. Run only on that go:
|
||||
The step runs under sudo's `secure_path`, so it sets its own PATH and umask.
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /bin/sh <<'MOSAIC_WRAPPER'
|
||||
cd / || exit 1
|
||||
umask 077
|
||||
PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
|
||||
mkdir -p -- "$HOME/.npm-global/bin" || exit 1
|
||||
"$HOME/src/mosaic-stack/scripts/stack-copy.sh" wrapper <sha> --create
|
||||
MOSAIC_WRAPPER
|
||||
```
|
||||
|
||||
`--create` refuses an existing wrapper; stop and review it rather than replacing
|
||||
it. The wrapper must name the same `<sha>` copy as the units.
|
||||
|
||||
Precheck lines: none. Root's checkout at `<sha>` and the stack's user's clone: precheck line missing.
|
||||
After install, `stack-copy.sh check --units external` reads the copy the units run.
|
||||
|
||||
The stack's user's `scripts/case2-install.sh` (C2-13b, design 10 step 2b) makes the first copy with
|
||||
`stack-copy.sh copy <sha>`. The copy runs `npm ci` from `package-lock.json`, which installs the pinned
|
||||
harness, so the host needs to reach the npm registry during the install. The install runs no
|
||||
`release.sh package` or `activate`.
|
||||
|
||||
## 7. The args file
|
||||
|
||||
`/etc/mosaic-stack/dev.args` holds the `case2-unit.sh` arguments, one per line, with the serve flags after a
|
||||
line holding `--`. Root writes it at the unit render, from the values the go names. The stack's user's
|
||||
install never writes it: that user can't write `/etc`, and root reads nothing that user wrote. It must be root's, with no group
|
||||
line holding `--`. I2's install writes it; ops-01 doesn't write it by hand. It must be root's, with no group
|
||||
or other write (`chmod 0644` or tighter). Before `--` it takes only `--user`, `--home`, `--business`,
|
||||
`--copy` (exactly once), `--node`, `--profile`, `--claude` and `--pm`. `stack-upgrade.sh` changes only the
|
||||
`--copy` line, and only after confirm passes.
|
||||
@@ -319,56 +272,16 @@ file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's bus
|
||||
|---|---|---|---|---|---|
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `ZAI_API_KEY=...` | 0600 | mosaic | library | `<path> sets ZAI_API_KEY` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names | 0600 or 0400 | mosaic | library | none. `case2-unit.sh` refuses at render. Precheck line missing. |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | After a unit start, wait for loopback `/healthz` with the bounded retry in section 10 before `case2-precheck.sh --business <business> --library-status`: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | C2-13b's `library.sync` (not landed) |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
|
||||
|
||||
The installer key, `<configDir>/secrets/hosts/<business>/<hostId>/installer`, is not a slot anyone places.
|
||||
Enrollment (section 8) writes it under the enrolling stack's config directory and deletes it when the host
|
||||
leaves.
|
||||
|
||||
## 9a. Notifier decision, before starting units
|
||||
|
||||
Make the explicit per-host decision before section 10: the default below is
|
||||
`binding: null`, so the bus runs without DMs. A named binding is a later per-host
|
||||
choice that needs Mos's go. The installer does not make this decision or write
|
||||
this file. Replace `<dataRoot>` with the configured data root and `<business>`
|
||||
with this host's business id. If the file already exists, stop and review the
|
||||
existing decision; this command refuses to overwrite it.
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /bin/sh <<'MOSAIC_NOTIFY'
|
||||
cd / || exit 1
|
||||
umask 077
|
||||
notify_root='<dataRoot>/notify'
|
||||
notify_dir="$notify_root/<business>"
|
||||
mkdir -p -m 0700 -- "$notify_root" && chmod 0700 -- "$notify_root" || exit 1
|
||||
mkdir -p -m 0700 -- "$notify_dir" && chmod 0700 -- "$notify_dir" || exit 1
|
||||
(set -C; printf '%s\n' '{"notifyVersion": 1, "binding": null}' > "$notify_dir/notify.json") || exit 1
|
||||
chmod 0600 -- "$notify_dir/notify.json"
|
||||
MOSAIC_NOTIFY
|
||||
```
|
||||
|
||||
Precheck line: `ok notifier config <dataRoot>/notify/<business>/notify.json validates (directory 0700, file 0600)`.
|
||||
|
||||
## 10. Console start
|
||||
|
||||
None by hand. I2's `case2-unit.sh` enables both units, starts the bus, then the Console (Q189, mos-1122).
|
||||
`stack-upgrade.sh` stops and starts both units itself.
|
||||
|
||||
After a unit start, wait for the Console's loopback `/healthz` to succeed before running
|
||||
`case2-precheck.sh`, including `--library-status` in section 9. Replace `<port>` with the installed
|
||||
Console unit's `--port`. This retry is bounded to 30 attempts, each with a 2-second timeout, with
|
||||
1 second between failed attempts. If it prints STOP, stop here; do not run the precheck or adopt.
|
||||
|
||||
```sh
|
||||
(
|
||||
attempts=0
|
||||
until curl -fsS --max-time 2 -o /dev/null 'http://127.0.0.1:<port>/healthz'; do
|
||||
attempts=$((attempts + 1))
|
||||
[ "$attempts" -lt 30 ] || { echo "STOP: loopback healthz did not become ready; do not continue" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
)
|
||||
```
|
||||
|
||||
Precheck line: none. Both units active after the render: precheck line missing.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Brief: a GitHub consumer for row 297's protected token callback
|
||||
|
||||
Sage, 2026-10-23, filed for Mos (mos-1525). Post-10-27.
|
||||
|
||||
## Brief: a GitHub consumer for Broker.githubWithToken
|
||||
|
||||
Row 297 builds the GitHub grant method and stops at the trusted host API
|
||||
`Broker.githubWithToken(business, agent, fn)` in `packages/bus/src/broker.mjs`.
|
||||
Nothing calls it yet: no git helper, mirror push or publication uses a GitHub
|
||||
token (violet BUILD.md, "Consumer gap for Sage's follow-up row").
|
||||
|
||||
This row adds the first consumer.
|
||||
|
||||
- It gets the token only through `githubWithToken`. It never reads the
|
||||
protected generation files, and never routes GitHub through the generic
|
||||
Gitea Credentials path, which has calendar-day dates and fork-specific
|
||||
identity.
|
||||
- It honors the callback's cancellation signal. The signal aborts on revoke and
|
||||
at the token safety deadline, so the consumer stops work and drops the token
|
||||
when it fires. No operation is retried with a token past its expiry.
|
||||
- No socket verb returns a token, and no consumer output, log or run record
|
||||
holds one.
|
||||
|
||||
Design comes first, with the consumer named (a git helper or mirror push), for
|
||||
Dewey's review. Tests cover: a revoke mid-call aborts the consumer; a call
|
||||
straddling the safety deadline aborts; an expired grant refuses before any
|
||||
network call; and no token text appears in output or the store.
|
||||
|
||||
Gate: Dewey approves the design and the manifest. The touched suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and mutants that drop the signal
|
||||
check or the expiry check are killed. Starts after row 297 is done.
|
||||
@@ -1,134 +0,0 @@
|
||||
# Brief: Case 1/2 host fixes for the 433 P1 rerun
|
||||
|
||||
Sage, 2026-10-23, filed for Mos (mos-1532). Issue #1816, Vikunja 614. On the 10-27 path.
|
||||
|
||||
## Brief: hostcheck item 5, bundle verify, case2-unit @, stack-copy cwd
|
||||
|
||||
ops-01 found two defects in the 433 P1 Run A on VM 1013 (ops-0256). Row 240 (c515273db) introduced the first one.
|
||||
|
||||
1. `scripts/case1-hostcheck.sh:117` compares `dpkg-statoverride --list /usr/bin/bwrap` to the literal
|
||||
`root mosaic 0750 /usr/bin/bwrap`. dpkg 1.22.6 on Ubuntu 24.04 prints `750`, so item 5 FAILs on every
|
||||
correctly built host. The apt hook and mosaic-hostcheck.service then close bwrap and write DRIFT. The fake
|
||||
at `packages/cli/tests/case1-hostcheck.test.mjs:73` writes `0750`, so the suite missed it.
|
||||
2. `docs/guides/host-run-sheet.md:192` and `:209` run `git bundle verify` outside a repository, which fails on
|
||||
a fresh host.
|
||||
|
||||
The fix:
|
||||
- Item 5 accepts `750` and `0750` as the same mode for the statoverride line. Owner, group and path stay exact,
|
||||
and every other mode still fails. The stat check at :118 is unchanged.
|
||||
- The run sheet verifies the bundle inside an empty bare scratch repo, as `scripts/stack-copy.sh:91-93` does.
|
||||
A failure there prints a FAIL line and does not `exit` the interactive root shell (Morgana N1, sage-3743).
|
||||
|
||||
Tests are append-only blocks in case1-hostcheck.test.mjs (mos-1272 pre-approved by Mos: 0 lines deleted, the :73
|
||||
fake unchanged). They cover: a `750` statoverride passes; `0750` still passes; `755`, `0755`, `7500`, `075`, `00750`, a tab
|
||||
or doubled space, and a wrong owner or group fail. Any other change to an existing test goes to Mos first.
|
||||
|
||||
Scope add (mos-1534):
|
||||
3. F3: `scripts/case2-unit.sh:62` plain() rejects `@`, so the run sheet's required `--claude` path
|
||||
(host-run-sheet.md:85-89, `@anthropic-ai`) refuses on every host (ops-0258, section 10). plain() accepts `@`.
|
||||
The :61 comment doesn't list it as unsafe, so no requirement changes.
|
||||
4. F4: `scripts/stack-copy.sh` fails when run as mosaic from a cwd mosaic can't read: the sheet's `sudo -u mosaic`
|
||||
lines and stack-upgrade.sh AS_USER at :154 and :293. It works from any cwd.
|
||||
Specs and test terms are in mos-1534. Mos pre-approved append-only test blocks in case2-unit.test.mjs under mos-1272.
|
||||
One new N carries F1 to F4 for the #433 rerun.
|
||||
|
||||
Owner Gladstone. Reviewer Morgana. Gate: Morgana approves. Mutants are killed: dropping the `750` form, widening the
|
||||
match to any mode, rejecting `@` again in plain(), and the F4 cwd fix reverted. The touched test files run whole, and Sage's full landing gate runs every
|
||||
package suite and every `scripts/test-*.sh`. After landing, Mos sends ops-01 the new N for a full 433 P1 rerun.
|
||||
No hostcheck install on .53 or .54 until it lands.
|
||||
|
||||
Scope add (mos-1536, ops-0260):
|
||||
5. F5: on a host that follows the run sheet, `mosaic-bus@<business>` exits 3 because nothing writes
|
||||
`<dataRoot>/notify/<business>/notify.json` (bus cli.mjs:104-122 requires it, README :876-892), and case2-precheck
|
||||
has no line for it, so it passed with the bus down. The run sheet gains a step that writes binding `null` as the
|
||||
default, and case2-precheck gains a notify line with tests. Install does not write the file: the README wants an
|
||||
explicit decision. A named binding for .53 is a later per-host choice that needs Mos's go. Spec in mos-1536.
|
||||
Gate adds a mutant: the precheck notify line removed or passing on a missing file. #1816 carries F1 to F5.
|
||||
|
||||
Scope add (mos-1537, ops-0261):
|
||||
- F5 fixture: Mos OK under mos-1272 for the host(t) notify fixture in case2-unit.test.mjs and the world(t) one in
|
||||
case2-install.test.mjs (a 0700 notify/acme dir and a null notify.json, with no assertion changed or deleted).
|
||||
Conditions: the negative F5 tests build their host without the file; one append-only install test shows
|
||||
notify.json bytes and mode are unchanged after install; the fixture lines are their own follow-up commit.
|
||||
- Section 9a creates both `notify/` and `notify/<business>` at 0700 (ops-0261 measured a 755 parent).
|
||||
6. F6: sheet :235 runs `stack-copy.sh check`, which requires `$HOME/.npm-global/bin/mosaic` (stack-copy.sh:160-173).
|
||||
Nothing creates it: install runs only `copy` (:178), bootstrap has no .npm-global, and the sheet has no wrapper
|
||||
step. The sheet gains a step after install: as mosaic from cwd `/`, create the bin dir, then run
|
||||
`wrapper <sha> --create`, naming the Mos gate (stack-copy.sh:3). No requirement change. #1816 carries F1 to F6.
|
||||
|
||||
|
||||
## Brief: F7, the wrapper step's PATH and umask under sudo
|
||||
|
||||
Row: F7 for the 433 P1 rerun (ops-0271, mos-1544). Docs only. Builder: a Codex thread. Reviewer: a separate Codex
|
||||
thread (mos-1542). It can land with any row and is not N+1.
|
||||
|
||||
Defect, from ops-01 on 094124b4e: the run sheet's wrapper step (`docs/guides/host-run-sheet.md` :249-253) fails
|
||||
rc=127 `node: command not found`. `sudo -u mosaic -H /bin/sh` runs under sudo's `secure_path`, which drops
|
||||
`/opt/mosaic-stack/node-<v>/bin`, and `scripts/stack-copy.sh wrapper` reaches `scripts/instance.sh:78`, which calls
|
||||
bare `node`. The heredoc also sets no umask, so `$HOME/.npm-global/bin` came out 0775.
|
||||
|
||||
Change, inside the `MOSAIC_WRAPPER` heredoc only:
|
||||
|
||||
1. Before the `mkdir`, add `umask 077`, the same way the notify step does (:325).
|
||||
2. Before the `mkdir`, add `PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH`, using the
|
||||
sheet's own PATH form (:69).
|
||||
3. Add one sentence above the block: the step runs under sudo's `secure_path`, so it sets its own PATH and umask.
|
||||
|
||||
Nothing else in the sheet changes, and no script changes. ops-01 is testing this exact form on 1013 (mos-1544).
|
||||
|
||||
Acceptance:
|
||||
|
||||
- The heredoc has `umask 077` and the PATH line before `mkdir`, with `<v>` written the same as the sheet's other
|
||||
node paths.
|
||||
- `git grep -n 'host-run-sheet' -- 'packages/*/tests/*' 'scripts/test-*.sh'` shows no test that reads the changed
|
||||
lines (positive control: a grep for a path a test does read). If a test does read them, stop and report.
|
||||
- BUILD.md under `agents/<seat>/work/queue-<row>/` records the diff and the grep.
|
||||
|
||||
F10, added to this row (ops-0274, mos-1548). Both are confirmed at 094124b4e.
|
||||
|
||||
- After a first install, the sheet has no `stack-release.sh adopt <business> --instance <i> --units external` step,
|
||||
so the first activate refuses `no-pointer` (`scripts/stack-release.sh:1297`, "no $POINTER; run adopt first"). Add
|
||||
that step where the sheet first moves from install to activate or upgrade. It runs on Mos's go, and Jason or Mos runs
|
||||
it (`stack-release.sh:14`).
|
||||
- The sheet doesn't say that the first `stack-upgrade.sh upgrade` after adopt needs `--no-rehearsal`. Adopt writes no
|
||||
backup, so without the flag the upgrade refuses `no-backup` (`scripts/stack-upgrade.sh:324-325`), and with the flag
|
||||
on a stack that has a backup it refuses `rehearsal-required`. Say so next to the upgrade command (sheet :210-223):
|
||||
only the first upgrade after adopt takes `--no-rehearsal`.
|
||||
|
||||
ops-01 is testing both on 1013 (mos-1548). F10 is also docs only. The grep in the acceptance list covers its lines too,
|
||||
and BUILD.md records them.
|
||||
|
||||
## Brief: F8 and F9, the library persona import runs without the clone's node_modules
|
||||
|
||||
Row: F8 and F9 for the 433 Run B rerun (ops-0273, mos-1546). Builder: a new Codex thread (mos-1542). Reviewer:
|
||||
Morgana (Claude), because the row touches the install path (mos-1542 rule 4). When it lands, ops-01 reruns Run B P1
|
||||
at that commit.
|
||||
|
||||
F8, confirmed at N = 094124b4e: `scripts/case2-install.sh:185` runs
|
||||
`node "$REPO/scripts/case2-persona-import.mjs"` from the clone (`REPO`, :38). `case2-persona-import.mjs:32` imports
|
||||
`yaml`, and a fresh clone has no `node_modules`, so the import fails `ERR_MODULE_NOT_FOUND`. The copy just made at
|
||||
`$COPIES/$COMMIT` (:178) has `node_modules` (mos-1546). ops-01 reproduced `ERR_MODULE_NOT_FOUND` from a checkout
|
||||
with no `node_modules`.
|
||||
|
||||
Fix F8 one of two ways, and say in BUILD.md which and why: run the importer from the copy
|
||||
(`$COPIES/$COMMIT/scripts/case2-persona-import.mjs`), or make node resolve its modules from the copy. The importer's
|
||||
relative import of `../packages/harness/src/bundle.mjs` must still resolve to the same commit's file. Whatever you
|
||||
pick has to work when the clone has no `node_modules` at all.
|
||||
|
||||
F9: :186 keeps only `tail -n 1` of the importer's output. For an uncaught node error that line is the
|
||||
node banner (`Node.js v24.22.1` on 1013, mos-1546), so the refusal hides the cause. Make the refusal carry the error line, for example the first line
|
||||
matching `Error` or `ERR_`, falling back to the last line. Keep it to one line. Don't put a full stack trace in the
|
||||
refusal.
|
||||
|
||||
Class check, same row: at N, `grep -n 'node "$REPO/' scripts/case2-install.sh scripts/case2-precheck.sh
|
||||
scripts/stack-copy.sh` finds only :185. Rerun that grep. Also list every clone-side `.mjs` those three scripts run
|
||||
that imports a package (a bare specifier, not `node:` or a relative path), and record the list in BUILD.md and the
|
||||
review request. Fix any other hit in this row only if it is the same defect, and say so.
|
||||
|
||||
Test: add a new, append-only test block to `packages/cli/tests/case2-install.test.mjs`. It runs the library persona
|
||||
import with no `node_modules` beside the script, and must fail at N. It needs no mos-1272 OK (mos-1546). Add a second
|
||||
assertion that the F9 refusal carries the error line, not `Node.js v...`. In BUILD.md, explain why the existing
|
||||
case2-install tests missed this. ops-01 thinks the checkout's `node_modules` hid it; check that.
|
||||
|
||||
Gate: Morgana approves the manifest. Every package suite (except queue) and every `scripts/test-*.sh` are green on
|
||||
Sage's gate, the new test fails on N, and a mutant that puts back `$REPO` at :185 is killed by the new test.
|
||||
@@ -678,36 +678,3 @@ claim aren't one (design D1's alternative). Bound to the next row that changes t
|
||||
Morgana, row 306 BUILD.md (#1813, landed 2d0318a44). A revoked run that also has a pending limit stop gets
|
||||
`#limitStop`'s `not-holder` on `message.receive` and `state`, because that check runs before `#agent`. It's
|
||||
rare and the run still stops. Bound to the next row that changes `#limitStop` or the order of those checks.
|
||||
|
||||
## Row226 N1: the install id regex is narrower than ID_PATTERN
|
||||
|
||||
Webby, row 226 round 1 (webby-0225, #1727, landed 661890e90). case2-install.sh:56 refuses a business id
|
||||
with `.` or `_`, which ID_PATTERN (vocabulary.mjs:199) and case2-precheck.sh:67 accept. Gosalyn confirmed
|
||||
it isn't deliberate (gosalyn-0208). It fails closed, so a real id with either character is refused, not
|
||||
mishandled. Bound to the next row that changes case2-install.sh: take the ID_PATTERN set, and add a test.
|
||||
|
||||
## Row226 N2: a first-run validate refusal leaves personas and the slot dir
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). After a validate refusal on a first run, the staged personas and the
|
||||
empty token slot dir stay behind, and a re-run counts them as present. Bound to the same follow-up as N1:
|
||||
remove what this run created, as the business file path already does (install.sh:198/:202/:218).
|
||||
|
||||
## Row226 N3: the run sheet doesn't pin the clone to the copy's sha
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). install.sh:217 validates with the checkout's scripts/mosaic, not the
|
||||
copy's, and host-run-sheet.md:237 never says the clone must be at `<sha>`. Bound to the same follow-up:
|
||||
a HEAD check in the install or one clause on the run sheet.
|
||||
|
||||
## Row226 N4: --library-status refuses after a 5 s lock wait
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). If the bus holds the library lock for more than 5 s,
|
||||
`case2-precheck.sh --library-status` refuses with `ERROR library-sync-busy`. A re-run clears it. Recorded
|
||||
as a known operator step, no fix bound.
|
||||
|
||||
## Row283 N1: SURVEY-RUN.md shows the scopes line without supporting
|
||||
|
||||
Webby, row 283 round 1 (webby-0267, #1788, review note queue-283/REVIEW-manifest-b2a1f4092.md on
|
||||
webby/reviews c6ea27aab). docs/case2/SURVEY-RUN.md:96 documents the `--mapping` line as
|
||||
`scopes canon .., playground .., unmapped ..; indexed ..`, which predates the supporting scope 283 adds.
|
||||
Nothing reads the file (decision 137). Bound to the next docs commit that touches SURVEY-RUN.md: add the
|
||||
supporting count to that line.
|
||||
+13
-17
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
<!-- mosaic-queue:begin -->
|
||||
|
||||
Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 3636 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -252,18 +252,18 @@ Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not e
|
||||
| 213 | Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | morgana; reviewers webby | #1714 | done | — | Webby approves the manifest. The business, bus and harness suites, `scripts/test-config.sh` and every `scripts/test-*.sh` are green on Sage's gate, and the `models` mutant is killed. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | LANDED 361ba2387 (39 paths incl. 4 packet files). Patch-id f313e69b6f7e = c771a36f4..2b3e67e36; verify-commit ok, 35 manifest paths. Same combined gate as 195: business 200/0 bus 542/0 harness 140/0 cli 663/0, test-config 31/0 and all 10 scripts green, models mutant L1 killed. Webby N2 is row 235 acceptance (mos-1163). |
|
||||
| 214 | Case 2 C2-11a editor wikilink node, after measuring whether [[ round-trips | herb; reviewers filbert | #1715 | in-progress | — | Filbert approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-11a editor wikilink node, after measuring whether [[ round-trips | — |
|
||||
| 215 | Case 2 C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | herb; reviewers filbert | #1716 | done | — | Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | LANDED e54e05463 on 6096b051b (sage-2094). Patch-id 2f68188b6c9d equals a36523713..dde3445ab; 118 paths, blobs and modes equal; tree 5b1613245 equals merge-tree. No path overlap with rows 66/243; no hash pins either way. Gate: Filbert APPROVE rev 2924; herb-0095 grant sage-2071 2634 pass 0 fail, 1 webui skip (editor rebuild needs esbuild 0.28.2, env, not a 215 path), accepted. |
|
||||
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | done | 268 done | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | LANDED fbe57a05b (replay of r5 dae76ccc2 onto 8e2885d26, 3 commits, 0 patch-id mismatches), pushed 21:09Z with 582b86935. Sage joint gate GREEN 21:05:34Z on fbe57a05b, every package suite but queue and every scripts/test-*.sh green. Filbert APPROVE r2 c31384 rev 3697. N3 (get returns a universe own supporting files) binds the C2-05c row; N1 doc, N2 universeInfo echo and BUILD.md pronoun nit go to a later docs commit. |
|
||||
| 217 | Case 2 C2-06 the citation guard, the page store, the runner texts and the retry | beakley; reviewers webby | #1718 | briefed | 268 done, 283 done | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the citation guard, the page store, the runner texts and the retry | — |
|
||||
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | briefed | 268 done | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | Rulings on phase A: agents/sage/work/queue-216/RULINGS.md (da9555ca2). r2 rebases on row 268's landing and adds the four PRD 1.14 playground tests. |
|
||||
| 217 | Case 2 C2-06 the citation guard, the page store, the runner texts and the retry | launchpad; reviewers webby | #1718 | briefed | 268 done | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the citation guard, the page store, the runner texts and the retry | — |
|
||||
| 218 | Case 2 C2-12 the setup sheet for Melanie and the survey run sheet | sage; reviewers webby | #1719 | done | — | Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and `scripts/test-*.sh` to confirm no test reads either file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12 the setup sheet for Melanie and the survey run sheet | LANDED f8276c5eb (pushed, on Darkwing sessions b65f3a0e8). Webby APPROVE r1-r3 (revs 2848, 2856, 2860; r3 candidate 1a38baf5af2a = 89ba79d94). Patch-id 18ba1e40ad29 seat 5c6f73ed7..89ba79d94 equals landed b65f3a0e8..f8276c5eb; three blobs equal 89ba79d94. Gate: doc only, 0 test readers (control 3). Melanie handover waits on C2-06 and Jason (#419). |
|
||||
| 219 | Case 2 C2-09a one thread per creative instance | rocko; reviewers dewey | #1720 | done | — | Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs` (`packages/bus/tests/channel-proof.test.mjs` pins a webui file). (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09a one thread per creative instance | LANDED 5fdfd7275 on refactor 10:40Z 10-23 (8 row commits cherry-picked onto 2efd127ce, range-diff all =, patch-ids match, tree e86a84a85 = gated merge 054da642f; top commit packet-only evidence). Gate manifest 4a2271dd: node 2523/0/2, ten shells 449/0, 24 mutants killed, audit ok (Rocko). Dewey approve rev 3507 c31108. N1 U6 persona label DEFERRED line follows. Slug fix Vikunja 590 and network leak 591 rows next. |
|
||||
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | done | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | LANDED fbe57a05b (replay of r5 739b868b6 onto 93694c518, 9 commits, 0 patch-id mismatches; tip 8e2885d26 for 220), pushed 21:09Z with 582b86935. Sage joint gate GREEN 21:05:34Z on fbe57a05b: every package suite but queue 0 fail 0 skipped, every scripts/test-*.sh rc 0. Filbert APPROVE r2 c31383 rev 3696. Researcher r5 reruns and mutants green. Filbert N1 (route test catalogue only) open, not blocking. |
|
||||
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | briefed | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | Rulings on phase A: agents/sage/work/queue-220/RULINGS.md (da9555ca2). Scope adds packages/business/src/library-seats.mjs (imported by path, no index export), its test, and control-board serve.mjs. r2 building as a Haiku subagent. |
|
||||
| 221 | Case 2 C2-20 Bean and Chet persona import, with a count-only dry run | gosalyn; reviewers webby | #1722 | done | — | Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20 Bean and Chet persona import, with a count-only dry run | LANDED 300766450, pushed. Cand 8f403d0d4, manifest 20f05e3a6ad5, 22 paths, verify-commit ok, patch-id dfbb754c6fb4 equals 62ddc96cc..8f403d0d4. Gate 11:07:50-11:19Z on b1fece6f0: test file alone 11/0, harness 140/0, cli 677/0, all scripts green. Brief Gate met: webby-0073 approve, test file alone, cli, scripts. No live run in the Gate; the import itself waits on Jason. N1 N2 N4 go to C2-20b, N3 to DEFERRED. |
|
||||
| 222 | Case 2 C2-03a fetch-only sync | megavolt; reviewers dewey | #1723 | done | — | Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every `scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03a fetch-only sync | sage-2024 on megavolt-0089: core.createObject=rename in the fetch argv is accepted as a brief departure, since C2-04 inspectMirror refuses nlink 2. Dewey checks it. The askpass env keys, export SAFE in project/src/git.mjs, bus/tests/library-sync.test.mjs and the SURVEY-RUN user line are accepted. The 5 s client timeout residual goes in BUILD.md and Dewey rules fix or defer. Slot after 197 proof 3 and the 249 gate. |
|
||||
| 223 | Case 2 C2-07 runner retry on a guard refusal, and the runner's notices | researcher; reviewers filbert | #1724 | blocked (from briefed): Q226: C2-07 folded into C2-06 (row 217). No work here; Jason parks it. | — | Filbert approves the manifest. The harness suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-07 folded into C2-06 | — |
|
||||
| 224 | Case 2 C2-05b broker lookup verbs and typed tools | della; reviewers dewey | #1725 | in-progress | 216 done | Dewey approves the manifest. The bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05b broker lookup verbs and typed tools | — |
|
||||
| 225 | Case 2 C2-10b Console chat: replies, citations, write notices and the status line | fethry; reviewers dewey | #1726 | briefed | 217 done | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10b Console chat: replies, citations, write notices and the status line | — |
|
||||
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | done | 215 done, 221 done, 222 done, 230 done | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | landed 661890e90 (replay of bfed0aa6c 893b4fd25 f3fabf51b onto 8352da88c, patch-ids equal, four blobs and modes equal to f3fabf51b) and fd3d2a99f (Darkwing 8a6321790 replay, mos-1519), tree 78fc5ad21 equals merge-tree. Webby APPROVE rev 3641. Sage gate GREEN 19:56Z on f3fabf51b: 17 package suites and 10 scripts, 0 fail. Webby N1-N4 DEFERRED e2b873390. #1727 closed. |
|
||||
| 227 | Case 2 C2-14 end-to-end acceptance on a scratch root | gizmoduck; reviewers dewey | #1728 | in-progress | — | Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-14 end-to-end acceptance on a scratch root | — |
|
||||
| 225 | Case 2 C2-10b Console chat: replies, citations, write notices and the status line | researcher; reviewers dewey | #1726 | briefed | — | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10b Console chat: replies, citations, write notices and the status line | — |
|
||||
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | in-progress | 215 done, 221 done, 222 done, 230 done | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | — |
|
||||
| 227 | Case 2 C2-14 end-to-end acceptance on a scratch root | launchpad; reviewers dewey | #1728 | briefed | — | Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-14 end-to-end acceptance on a scratch root | — |
|
||||
| 228 | Row A: ZAI_API_KEY passes through ENV_ALLOW to sessions, README line, pass-through test (XS, Haiku high) | researcher; reviewers webby | #1729 | done | — | Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on Sage's gate with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row A, ZAI_API_KEY in the session env allowlist | Landed 86249ac55 (pushed 07:25Z) on 59f817c32: the 3 manifest paths from cf18c42b9. Patch-id b6ffe31feb0d equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. |
|
||||
| 229 | Row B: per-instance pi agent dir for Chet's Codex login, design only, starts after 10-27 (#428) | gosalyn; reviewers webby | #1730 | briefed | — | A live check, with Jason's or Mos's go, that two clients on one ChatGPT login don't revoke each other's refresh tokens. No build row starts before it passes. Held until after 10-27. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row B, a per-instance pi agent dir for Chet's Codex login (#428) | — |
|
||||
| 230 | Case 2 C2-13a unit render and host precheck (case2-unit.sh, case2-precheck.sh) | gosalyn; reviewers webby | #1732 | done | — | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every scripts/test-*.sh. A run on a host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13a unit render and host precheck | Landed c8bc5cf4c on refactor (base 390ea5261), pushed. Patch-id f4b49eb68997 matches git diff 722136772..96768766c on the 5 manifest paths (manifest fbb0d7e42c67, Webby approve r2 rev 2563). packages/cli/README.md 3-way with row 199, hunks apart. Gosalyn gate on 96768766c: cli 541/0, scripts 429/0, mutants 10/10. Merged tree check: case2-unit 30/0, host 42/0. |
|
||||
@@ -275,7 +275,7 @@ Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not e
|
||||
| 236 | #436b Gitea grant path build: seat accounts and tokens through an approved grant, schema 3j-3k (design r5 1be535aca, due 10-27 at risk; live waits for Jason's section 8 approval and bootstrap step) | megavolt; reviewers dewey | #1742 | done | 210 done, 234 done | Dewey approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. Section 9's mutants are killed, N9's restart test included. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: #436b Gitea grant path, build (schema 3j-3k) | Landed bd5f11ded on refactor, a fast-forward on be2b008cc. It is 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =) plus 2 stale-pin fixes and a gitea-not-configured raise test (sage-2559). Dewey approve on manifest 0b00269b. Megavolt gate under Sage CLEAR: 2c green, bus 765, business 228, cli 832, ten scripts rc=0, mutants 40/40; skips named, env-only. Overlap 13/257 clean. Schema 3j-3k. #1742 closed. |
|
||||
| 237 | Row 199 follow-up N1: one decision publishes once (publish-busy or #spent recheck) | megavolt; reviewers duckworth | #1743 | done | 199 done | Duckworth approves the manifest. The bus suite and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 199 follow-up N1, one decision publishes once | Landed d7d943199 on refactor (local, push pending), patch-id c5f4beb4 = e42415935, manifest bb777cb0 blobs checked. Sage gate 01:31-01:44Z on 816d48b03+4 paths: M1 killed (bus 1/13, cli 1/8), every package suite and scripts/test-*.sh green (bus 478, cli 564). |
|
||||
| 238 | row 200 follow-up N1: the sso-config hint carries --instance NAME (#1744) | launchpad; reviewers webby | #1744 | done | 200 done | Webby approves the manifest; stack-release.test.mjs whole, cli suite and every scripts/test-*.sh green on Sage's gate; the mutant killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: row 200 follow-up N1, the sso-config hint names the instance | Landed d49b1f2bd (pushed 07:25Z) on 86249ac55: the 3 manifest paths from 4c90099ea. Patch-id b9d0bb0ee3c7 equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. Mutant (sso-config drops --instance) killed, survived none. Round 1 N1 stays on the #1744 carry list. |
|
||||
| 239 | Row 194 follow-up: the credential revoke runs under the writer's lock, refusals after a revoke say so, and a stale-revision fire test (duckworth-0074, mos-1158) | honker; reviewers duckworth | #1745 | done | 194 done | Duckworth approves the manifest. The bus, business, cli and webui suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: row 194 follow-up, the revoke under the writer's lock and a fire test | LANDED e0cb1db2c (ff from 546366f4a) 22:24:58Z 10-23. Duckworth APPROVE c31422 rev 3723. Honker slot: touched files 65/65, mutants 9/9. Sage full gate GREEN 22:24:24Z: all package suites 0 fail (bus 909, cli 976, plugins 119, webui 411) and every scripts/test-*.sh rc 0; skips are queue canonical-root and task live-provider only. Overlap: 298 shares broker.mjs, runtime.mjs, instance-settings.mjs and rebases. N+1 for 433 P2/P3 (mos-1540). |
|
||||
| 239 | Row 194 follow-up: the credential revoke runs under the writer's lock, refusals after a revoke say so, and a stale-revision fire test (duckworth-0074, mos-1158) | honker; reviewers duckworth | #1745 | in-progress | 194 done | Duckworth approves the manifest. The bus, business, cli and webui suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: row 194 follow-up, the revoke under the writer's lock and a fire test | — |
|
||||
| 240 | Installer I4: hostcheck, the bwrap profile check, pins and boot unit, from r6 B2 (MVP, Q187, Vikunja #432; split from I3) | darkwing; reviewers filbert | #1746 | done | 233 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) (sage) | `docs/plans/2026-10-20_installer-rows.md` § I4 hostcheck: the bwrap profile check, pins and boot unit | Landed c515273db (pushed 06:57Z) on da3051ccc: the 9 manifest paths from ad1b23ab2. Patch-id 48e376efe8a4 equals the candidate's; modes and blobs match; verify-commit ok (9 paths). Sage's gate on 060a17b54 plus the 9 paths, 06:45:05Z to 06:56:21Z: cli 639/0, all ten scripts/test-*.sh 585/0. Darkwing's r2: singles 37/0, M1-M8 8/8. Filbert N1 and N2 in DEFERRED (da3051ccc); N3 by design. |
|
||||
| 241 | Row 237 follow-up: one decision lands once, a two-ticket broker land test and three mutants on the landing guards (megavolt-0069, sage-1651, P19 441) | megavolt; reviewers duckworth | #1747 | done | 237 done | Duckworth approves the manifest. The bus, seat and cli suites and every scripts/test-*.sh are green on Sage's gate, and the three mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up, one decision lands once, with tests at the broker | Landed 5d4a61776 on refactor, pushed (base e65a3ab4d). Blobs from 3e3ed303e on 2 paths; patch-id a1649f80 equals git diff f5753c0e8..3e3ed303e on those paths; ls-tree modes equal; manifest e8266864 verifies on the landed files. Sage slot on bbf3b1ee8: M1-M3 killed; gate 15 packages 2797/0, 10 script suites 429/0. Duckworth approve 29870 rev 2628. |
|
||||
| 242 | Row 237 follow-up N1: a retry after publish-timeout pushes once; hold the decision or kill and wait for the push, a slow-push retry test and a mutant (duckworth-0069, P19 440) | megavolt; reviewers duckworth | #1748 | done | 237 done | Duckworth approves the manifest. The bus and cli suites and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up N1, a retry after publish-timeout pushes once | Landed 5fb736b92 on refactor, pushed (base 96bac588f). Blobs from 635c10924 on 3 paths; patch-id ceb741ee equals git diff 8fede5fc6..635c10924 on those paths; ls-tree modes equal; manifest 1fbfab6c verifies on the landed files. Megavolt M1/M2 killed on Sage's slot; Sage gate 15 packages 2826/0, 10 script suites 429/0. Duckworth approve 29889 rev 2639. |
|
||||
@@ -319,12 +319,12 @@ Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not e
|
||||
| 280 | Bus tests: the scratch sshd proves it owns its port (194 gate follow-up, MVP, mos-1270) | rocko; reviewers duckworth | #1785 | done | — | Duckworth approves the manifest. Both files run whole, the bus suite is green on Sage's gate, the 30-run repeat has no timeout, and M1 is killed. Row 194 b17's full gate then runs on top. Any red goes to Mos (mos-1270). (sage) | `docs/plans/2026-10-22_sshd-port-race.md` § Brief: the scratch sshd's port is ours before a test uses it | Landed b0fd28b6f 07:57:30Z, ff from da5d3651d: b765f5cb8 (Duckworth approve rev 3153, manifest 27806c05) + slim packet. Sage gate bus 656/0/0, ge 41, hr 26 (c30431). Repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Full evidence d6c924764 on rocko/row280-da5d-20261022. N2-N4 carry in BUILD.md. 194 b17 rebases on b0fd28b6f. |
|
||||
| 281 | A fire or a delete revokes the instance's host and Gitea grants (194/236 follow-up, MVP, mos-1271) | megavolt; reviewers dewey | #1786 | done | 236 done | Dewey approves the design, then the manifest. Bus, business, cli and every touched test file run whole, plus the Docker gitea-grant test and every scripts/test-*.sh, green on Sage's gate. M1-M4 killed. Lands before .53 Case 1 hires a granted instance. Red goes to Sage, then Mos. (sage) | `docs/plans/2026-10-22_fire-revokes-grants.md` § Brief: no grant outlives its instance | LANDED e479521d3 ff on 385a7194c (17 commits, manifest b73d4c81). Slot on 7979fb41e green: bus 819, business 231, cli 837, dewey-bus 76 with gitea-grant-docker run, shells green under 3x limits, M1-M22 killed. M1 hang fixed in b4a (mos-1273). Skips: test-task row 244 live-provider pair, test-queue canonical root. DEFERRED: N1 host-unknown hint verb, N2 fire tool text, to the follow-up row. Dewey APPROVE c30614. |
|
||||
| 282 | PRD REQ-LIB-6 rewritten to Q223 and Q226 (mos-1320 item 8) | gosalyn; reviewers webby | #1787 | done | — | Webby approves against the ruling. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Jason gets a report. (sage) | `docs/plans/2026-10-20_case2-rows.md` § PRD REQ-LIB-6 rewritten to Q223 and Q226 (mos-1320 item 8) | Landed 647424442 on 86a665bdf: 1841f7d4e and 6b5b06819 rebased, patch-ids equal (4e50b4b6, 16a7a02f), range-diff =, tree 4f30ed10a = Gosalyn's gated trial tree. Webby APPROVE rev 3308 c30764 on 6b5b06819. Gate 19:06Z on the tip tree: bus+tasks requirements and tasks verbs 37/0/0, 0 skips; no scripts/test-*.sh reads the PRD (control ok). PRD 1.15. 1.16 is row 291, waits for Jason. |
|
||||
| 283 | Case 2 C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | honker; reviewers webby | #1788 | done | 268 done, 282 done | Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | LANDED 8d2856ee3 (replay of 0bf308dba onto 59226f878, 13 patch-ids equal) plus DEFERRED N1 93694c518. Webby APPROVE r1 c31338 rev 3677, r2 c31342 rev 3683 (mos-1528 G1 mapping.test:52). Sage full gate GREEN 20:37:16Z: all package suites but queue plus every scripts/test-*.sh, 0 fail, library 98/0, webui npm ci, editor build required. supporting.test first-run red placed (mos-1272, 54d9e07bb). #1788 closed c31358. Live unchanged 45dca69f6. |
|
||||
| 283 | Case 2 C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | honker; reviewers webby | #1788 | briefed | 268 done, 282 done | Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | — |
|
||||
| 284 | Case 2 C2-05c the supporting scope and blob in the lookups | della; reviewers dewey | #1789 | briefed | 224 done, 283 done | Dewey approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate. Grep packages/*/tests for the scope enum pin and ['canon', 'playground'] before in-review; update each. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05c the supporting scope and blob in the lookups (sage-2833) | — |
|
||||
| 285 | Case 2 C2-08b role changes for agent writes and the chief's read grant | scrooge; reviewers webby | #1790 | briefed | 213 done, 291 done | Webby approves the manifest. Business suite, test-config.sh and every scripts/test-*.sh green on Sage's gate. Grep for CREATIVE_KINDS and 64-hex pins of changed role files. Sage checks it against mos-1309 item 5. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08b role changes for agent writes and the chief's read grant (mos-1309 item 5) | — |
|
||||
| 286 | Case 2 C2-03b push and per-seat credentials for the library mirror (with R4, mos-1323) | ludwig; reviewers dewey | #1791 | briefed | 222 done | Dewey approves the manifest. Library and bus suites and every scripts/test-*.sh green on Sage's gate, mutant killed. A search of the run output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03b push and per-seat credentials for the library mirror | — |
|
||||
| 287 | Case 2 C2-21 the grant executor for a library project | megavolt; reviewers dewey | #1792 | briefed | 226 done, 285 done | Dewey approves the manifest. Bus suite, Gitea Docker test, install test and every scripts/test-*.sh green on Sage's gate. A search of the run output for the fixture admin and seat tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-21 the grant executor for a library project | — |
|
||||
| 288 | Case 2 C2-02 agent writes, the notice, revert and her Console saves | bubba; reviewers webby | #1793 | briefed | 215 done, 282 done, 283 done, 284 done, 285 done, 286 done | Webby approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate, mutants killed. A search of the run output for the fixture tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-02 agent writes, the notice, revert and her Console saves | A2 revert contract (morgana-0199, sage-3596): library.revert is a human verb. Args {project, commit}, strict keys; project is the business.projects key (the chat repo link), not a UUID. Result {project, path, reverted, commit}. Refusals: 409 stale-base, and 409 library-revert-not-agent for an agent caller. Owner Bubba under Q245 (mos-1506). |
|
||||
| 288 | Case 2 C2-02 agent writes, the notice, revert and her Console saves | honker; reviewers webby | #1793 | briefed | 215 done, 282 done, 283 done, 284 done, 285 done, 286 done | Webby approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate, mutants killed. A search of the run output for the fixture tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-02 agent writes, the notice, revert and her Console saves | — |
|
||||
| 289 | Case 2 C2-11b Console library pages and her saves (Haiku subagent, decision 129) | researcher; reviewers filbert | #1794 | briefed | 214 done, 288 done | Filbert approves the manifest. The webui suite and every scripts/test-*.sh are green on Sage's gate. Grep for hash pins on bus.js and serve.mjs before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-11b Console library pages and her saves | — |
|
||||
| 290 | Case 2 C2-12b the setup sheet rewrite for free chat, supporting folders and saves | louie; reviewers webby | #1795 | briefed | 217 done, 218 done, 285 done, 291 done | Doc only. Webby approves it against the ruling. Before landing, grep packages/*/tests and scripts/test-*.sh to confirm no test reads the file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12b the setup sheet rewrite for free chat, supporting folders and saves | — |
|
||||
| 291 | PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | gosalyn; reviewers webby | #1796 | done | 282 done | Webby approves the text. Jason says yes to the exact text (Mos asks him, mos-1328 step 3); no landing before that. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Mos gets the landing commit. (sage) | `docs/plans/2026-10-22_prd-1-16-brief.md` § PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | Landed 4874d1e83 on e909ef51a: 482e865ac replayed, patch-id equal (cef7de74), range-diff =. Webby APPROVE c30848 on 482e865ac; Jason approved the text in Q229. Gate 16:15Z on the tip tree: every package suite but queue, 0 fail 0 skipped (tasks 100, bus 891, cli 941, plugins 119, webui 392, business 231), every scripts/test-*.sh rc 0 (queue-root and row 244 skips only). PRD 1.16. 1.17 is row 308. |
|
||||
@@ -332,9 +332,9 @@ Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not e
|
||||
| 293 | 574: maintenance-free test fixture commits (Vikunja 574) | gander; reviewers filbert | #1805 | done | — | Filbert approves. The four whole files, then the seat and cli suites, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 574, maintenance-free test fixture commits (Vikunja 574) | LANDED e51d11640 03:55Z 10-23 (cherry-pick of gander b23783f39..a9cae2bd6 onto 464010fbe; range-diff 6/6 equal, range patch-id equal). Filbert APPROVE c30982 cdda41f5; run 2 4 files 153/0, seat 202/0, cli 905/0, 312 flagged/0 maintenance children. N1: lifecycle.test needs TMPDIR of 50 bytes or less. Residual: queue store.mjs, discord git.mjs, harness tools.mjs unflagged. #1805 closed c31013, P19 574 c888. |
|
||||
| 294 | FED R6: Console granted-user sign-in, user-cli transport, peer links | launchpad; reviewers duckworth | #1800 | done | — | Duckworth approves the manifest. The webui, cli and bus suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R6, Console granted-user sign-in | landed d3059c2c3 on refactor 07:04Z: cherry-pick of c902a767d..24b745447 onto 09d0d6faf, range-diff 11 =, patch-id 8371405a equal. Duckworth approve c31053 rev 3469 on a1ff5d67, C1/C2 met. Run 4 on 8616b9a4f from npm ci: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree: stack-release-external 25, stack-upgrade 21, stack-release 223/224 (R9 transient, row 1810) then 224/224 rerun under mos-1422. Follow-ups rows 300, 301. Closes #1800. |
|
||||
| 295 | FED R4: revocation | morgana; reviewers dewey | #1801 | done | — | Dewey approves the manifest. The bus, cli and business suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R4, revocation | LANDED 2efd127ce on refactor 10:10Z 10-23 (9 commits cherry-picked onto 07802dc57, range-diff all =, patch-ids match, HEAD~1 tree a86592ffc = gated tree, top commit BUILD.md doc-only). Gate d68b4300d manifest e94c9aeb: 13 whole files, bus 886/0, business 231/0, cli 924/0, harness 151/0, all test-*.sh rc 0, M1-M16 killed, M8 per mos-1436. Dewey approve rev 3500 c31102. Re-pin precheck mos-1436 cond 11 binds. |
|
||||
| 296 | FED R7: scratch two-instance acceptance | lena; reviewers filbert | #1802 | briefed | 190 done, 294 done, 295 done | Filbert approves the manifest. The script test and every scripts/test-*.sh are green on Sage's gate, and no unit, port or socket of the script's outlives the run. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R7, scratch two-instance acceptance | — |
|
||||
| 297 | GitHub grant method (Bushroot's half of #436b) | violet; reviewers dewey | #1803 | briefed | 198 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the GitHub grant method (Bushroot's half of #436b) | — |
|
||||
| 298 | Q228: chief-of-staff grant scope | boyd; reviewers dewey | #1804 | in-review, round 1, request posted | 252 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the chief-of-staff grant scope (Q228) | — |
|
||||
| 296 | FED R7: scratch two-instance acceptance | quackerjack; reviewers filbert | #1802 | briefed | 190 done, 294 done, 295 done | Filbert approves the manifest. The script test and every scripts/test-*.sh are green on Sage's gate, and no unit, port or socket of the script's outlives the run. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R7, scratch two-instance acceptance | — |
|
||||
| 297 | GitHub grant method (Bushroot's half of #436b) | bushroot; reviewers dewey | #1803 | briefed | 198 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the GitHub grant method (Bushroot's half of #436b) | — |
|
||||
| 298 | Q228: chief-of-staff grant scope | megavolt; reviewers dewey | #1804 | briefed | 252 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the chief-of-staff grant scope (Q228) | — |
|
||||
| 299 | 573: the P10 hop band (Vikunja 573) | gander; reviewers filbert | #1806 | done | 293 done | Filbert approves. The whole file under load at least 3 times, then the cli suite, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 573, the P10 hop band (Vikunja 573) | landed ffb2fedfb 08:03Z on 15e688cc0 via Filbert SESSIONS c1f910b4c; 5 commits = gander/row299-gate-277ace0b 63db96d8b (range-diff 5/5 =, tree differs only BUILD-LOG/SESSIONS); manifest 277ace0b sha256 -c OK. Gate green on ffa93b054: case1 whole 3x 53/0/0, cli whole 917/0/0 (59 files), P10 inner 2.27 outer 4.01, M1-M6 killed (gander-0189). Filbert approve rev 3485 c31084; N1 M1 1.23 s margin. |
|
||||
| 300 | R6-N1: HTTP-level test of the user-session option swap (294 N1) | launchpad; reviewers duckworth | #1807 | done | 294 done | Duckworth approves the manifest; new test file whole, webui suite, mutant M12 killed (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N1, an HTTP-level test of the user-session option swap | Landed on refactor as 9d929911c..07315e651 (from launchpad/row300 a00b25b37, range-diff equal), pushed 5b846b048 12:28Z. Duckworth approved r2 (c31158, rev 3559). Sage gate on bcf3c1dac: user-option-swap 1/0, webui 375/0 with pinned esbuild and MOSAIC_REQUIRE_EDITOR_BUILD=1, M12 killed at :151. Run 1 red placed under mos-1272 (headers). N1 deferred: board proxy upstream has no recorder. |
|
||||
| 301 | R6-N4: fold user-bus.mjs back into bus.mjs (294 N4) | launchpad; reviewers duckworth | #1808 | done | 294 done | Duckworth approves the manifest; channel-proof.test, webui suite and 294 user-transport tests whole and green; re-pin only with Mos go (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N4, fold user-bus.mjs back into bus.mjs | Landed on refactor as bb031ec84..5b846b048 (from launchpad/row301 acc97fb9c, range-diff equal), pushed 12:28Z. Re-pin of webui bus.mjs to 39fcb27c is its own commit 59f0e44f4 (mos-1449); fixture stubs per mos-1451. Duckworth approved r2 (c31159, rev 3560). Sage gate: channel-proof 2/0, webui 374/0 pinned esbuild, granted-user 19/0. Live unchanged; 301 makes its own re-pin window. |
|
||||
@@ -355,10 +355,6 @@ Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not e
|
||||
| 316 | 571 W5: read-only flags, citations and chat suggestions (post-10-27) | unassigned | — | queued | 315 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W5 read-only flags, citations and chat suggestions | — |
|
||||
| 317 | 571 W7: human one-way chapter import and Case 2 format routing, production import closed; the first real import opens only after W8 lands (post-10-27, mos-1470) | unassigned | — | queued | 314 done, 315 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W7 human one-way chapter import and Case 2 format routing | — |
|
||||
| 318 | 571 W8: Console Connections view for Markdown and DOCX story files, before W7's first real import (post-10-27, mos-1470) | unassigned | — | queued | 314 done, 315 done, 317 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W8 Console Connections view for Markdown and DOCX story files | fenton-0017 carry: connections.test.mjs must test that links inside comments and tracked deletions are excluded. W8 follows W7's closed routing candidate and lands before W7's first real import opens (mos-1470) |
|
||||
| 319 | GitHub consumer for row 297's protected githubWithToken callback, honoring expiry and cancellation (post-10-27) | unassigned; reviewers dewey | — | queued | 297 done | Dewey approves the design and the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the signal and expiry mutants are killed. (sage) | `docs/plans/2026-10-23_github-consumer-brief.md` § Brief: a GitHub consumer for Broker.githubWithToken | — |
|
||||
| 320 | Case 1/2 host fixes for the 433 P1 rerun: hostcheck item 5 mode 750, run sheet bundle verify, case2-unit --claude @, stack-copy unreadable cwd, notify.json step and precheck, mosaic wrapper step (10-27 path) | gladstone; reviewers morgana | #1816 | required; done | — | Morgana approves. Touched test files run whole (case1-hostcheck, case2-unit, case2-install, stack-copy) and the 8 named mutants are killed; Sage's full landing gate runs every package suite and every scripts/test-*.sh. After landing Mos sends ops-01 the new N. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: hostcheck item 5, bundle verify, case2-unit @, stack-copy cwd | LANDED 094124b4e 21:53Z (replay of faf55618e onto 45e3ca7bd, code paths patch-id equal, log appends in one final commit). F1-F6 hostcheck item 5, bundle verify, case2-unit scoped paths and cwd, precheck notify decision, run sheet 9a notify dirs 0700, wrapper step. Morgana APPROVE r2 c31403 rev 3715. Full gate GREEN 21:53:00Z, 0 fail (mos-1453/1455/1456). Vikunja 614 done. 433 P1 reruns on this N (mos-1532). |
|
||||
| 321 | F7 and F10: host run sheet wrapper step sets umask 077 and the node PATH under sudo secure_path; adopt step after a first install; --no-rehearsal on the first upgrade after adopt (433, ops-0271/0274); docs only | gladstone; reviewers boyd | #1817 | required; briefed | — | Boyd (Codex) approves. A git grep with a positive control shows no test reads the changed run sheet lines; BUILD.md records the diff and the grep. Doc only, lands with any row (mos-1544). (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F7, the wrapper step's PATH and umask under sudo | — |
|
||||
| 322 | F8/F9: case2-install.sh runs the library persona import from the stack copy, and its refusal carries the error line (433 Run B, ops-0273) | goldie; reviewers morgana | #1818 | required; briefed | — | Morgana approves (install path). The new case2-install test fails on 094124b4e and passes on the candidate; a mutant restoring REPO at :185 is killed; Sage's full landing gate runs every package suite and scripts/test-*.sh. Then ops-01 reruns Run B P1. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F8 and F9, the library persona import runs without the clone's node_modules | — |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
|
||||
+50
-3117
File diff suppressed because it is too large.
Load diff
@@ -2455,13 +2455,11 @@ export class Broker {
|
||||
} catch (e) {
|
||||
const code = e instanceof BusError ? e.code : 'outcome-unknown';
|
||||
const list = out.grants.map((g) => g.grant).join(', ') || 'none';
|
||||
// Row 239 (#1745): a step B refusal after the writer's revoke ran.
|
||||
const tokens = code.endsWith('-revoked') ? ' Its credential tokens may already be revoked.' : '';
|
||||
this.#store.transaction(() => {
|
||||
this.#event(sc, 'action.allowed', { action: 'fire.failed', job, step, code }, name);
|
||||
this.#humanNotice(
|
||||
sc.business,
|
||||
`Fire of ${name}: job ${job} revoked its grants (${list}) but stopped at step ${step} (${code}).${tokens} The instance is still firing. Run the fire again once ${code} is cleared.`,
|
||||
`Fire of ${name}: job ${job} revoked its grants (${list}) but stopped at step ${step} (${code}). The instance is still firing. Run the fire again once ${code} is cleared.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -4931,9 +4929,6 @@ export class Broker {
|
||||
// The bus code for an organization writer's refusal. Anything the writer didn't refuse by name is
|
||||
// outcome-unknown: the write may have committed.
|
||||
#organizationFail(e, verb) {
|
||||
// Row 239 (#1745): the writer marks an error raised after a fire's or a delete's revoke ran, so
|
||||
// the human reads that the tokens may already be revoked. Without the mark, today's code.
|
||||
if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");
|
||||
if (e.code === "revision-conflict") fail("revision-conflict");
|
||||
if (e.code?.startsWith("organization-")) fail(e.code);
|
||||
if (e.code === "invalid-plugins" || e.code === "unknown-plugin") fail(e.code);
|
||||
|
||||
@@ -161,11 +161,11 @@ export async function startBroker({
|
||||
const levels=organization?await harnessLevels():()=>null;
|
||||
const settings=organization?launchSettings({...organization,sessions:()=>readSessions(dataRoot),levels}):null;
|
||||
// Row 194 (#1692, brief item 8, row 199 D13): a fire or a delete revokes the agent's credentials
|
||||
// after the writer's checks and before its write, under the writer's lock (row 239). Fire revokes
|
||||
// each service of the entry and keeps the store directory, delete calls remove (sage-1593). A
|
||||
// service the running bus doesn't know (an agent created since the start) has nothing in memory,
|
||||
// so it goes on, and then no token or tmp file may remain in the agent's store directory
|
||||
// (sage-1592). Any failure stops the verb with the file unchanged.
|
||||
// after the writer's checks and before its write. Fire revokes each service of the entry and keeps
|
||||
// the store directory, delete calls remove (sage-1593). A service the running bus doesn't know (an
|
||||
// agent created since the start) has nothing in memory, so it goes on, and then no token or tmp
|
||||
// file may remain in the agent's store directory (sage-1592). Any failure stops the verb with the
|
||||
// file unchanged.
|
||||
const revokeFailed = (message) => new OrganizationError('credential-revoke-failed', message);
|
||||
function revoke(business, agent, how, services) {
|
||||
const instance = business + '/' + agent;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
// 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";
|
||||
@@ -20,8 +22,7 @@ import { Client } from "../src/client.mjs";
|
||||
const baselines = {
|
||||
"packages/bus/src/human.mjs": "bb13c146717cdc46d75d157177e400edac4d3e10f14fe511027a25fd44cec885",
|
||||
"packages/bus/src/human-cli.mjs": "271b9781d73cf31cc562a56180e3ebcd725b5fde148a83347a201fdef2fc28af",
|
||||
// Row 239 (#1745): EXIT outcome-unknown-revoked 1; no proof or transport change.
|
||||
"packages/cli/src/transport.mjs": "662642a8ce739fcb6c188ad07125a94e008c8a305eced127c8041d472c56e55b",
|
||||
"packages/cli/src/transport.mjs": "5129c44b4d9f642e825344df65c9bfb4bac4fc8d6b25343b42d39853dbd40774",
|
||||
// Row 165 (#1662): seven STATUS entries for the role instance routes (organization-instance-exists,
|
||||
// organization-unknown-instance, organization-credentials-out-of-scope, organization-launch-not-configured,
|
||||
// organization-no-change, organization-project-mismatch, organization-unresolvable-instance), no proof or
|
||||
@@ -30,9 +31,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.
|
||||
// Row 239 (#1745): outcome-unknown-revoked joins OPEN (duckworth-0165 C1), and STATUS gets
|
||||
// revision-conflict-revoked 409 and outcome-unknown-revoked 503; no proof or transport change.
|
||||
"packages/webui/src/bus.mjs": "e3f52172054676b7982eda420ce051854d404c3f0f81834c3f81138bd91c2a89",
|
||||
"packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
|
||||
};
|
||||
const repoFile = path => new URL("../../../" + path, import.meta.url);
|
||||
|
||||
|
||||
@@ -133,9 +133,8 @@ async function setup(t) {
|
||||
},
|
||||
};
|
||||
// The fake organization writer. A fire delists the name and a delete drops the role, each with a
|
||||
// reload, as the runtime reloads after a commit. `fireError` makes the fire's write refuse, and
|
||||
// `deleteError` the delete's (row 239).
|
||||
const org = { log: [], fireError: null, deleteError: null };
|
||||
// reload, as the runtime reloads after a commit. `fireError` makes the fire's write refuse.
|
||||
const org = { log: [], fireError: null };
|
||||
const sw = { state: { version: 1, halted: false, generation: GENERATION } };
|
||||
let broker;
|
||||
const organization = (business, op, args) => {
|
||||
@@ -147,7 +146,6 @@ async function setup(t) {
|
||||
return { revision: REV, transaction: 't-' + org.log.length };
|
||||
}
|
||||
if (op === 'instances.set') {
|
||||
if (org.deleteError) throw org.deleteError;
|
||||
delete doc.roles[args.instance];
|
||||
doc.launch.instances = doc.launch.instances.filter((n) => n !== args.instance);
|
||||
broker.reloadBusiness('demo', doc);
|
||||
@@ -954,44 +952,3 @@ test("row 281 T14 (N1, sage-2653): a host removed under an undecided decision st
|
||||
assert.ok(!f.doc.launch.instances.includes('coder'));
|
||||
assert.equal(f.grantEvents().filter((e) => e.action === 'grant.revoke.refused').length, 1);
|
||||
});
|
||||
|
||||
// Row 239 (#1745): the writer marks an error raised after its own revoke ran (revoked: true), and the
|
||||
// bus names it, so the human reads that the tokens may already be revoked. Unmarked, today's code.
|
||||
test("row 239 T4: a refusal after the writer's revoke is named -revoked, in the job's fire.failed and notice, a synchronous fire and a delete", async (t) => {
|
||||
const f = await setup(t);
|
||||
await f.approveHost('coder', 'web1');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict', revoked: true });
|
||||
const out = f.fire('coder');
|
||||
await f.broker.grantsIdle();
|
||||
const [failed] = f.allowed('fire.failed');
|
||||
assert.deepEqual([failed.body.job, failed.body.step, failed.body.code], [out.job, 'B', 'revision-conflict-revoked']);
|
||||
const [message] = f.inbox();
|
||||
assert.ok(message.includes('(revision-conflict-revoked). Its credential tokens may already be revoked. The instance is still firing.'), message);
|
||||
// helper has no grant, so its fire writes synchronously and answers with the code.
|
||||
await rejects(() => f.fire('helper'), 'revision-conflict-revoked');
|
||||
// Any other marked error: the write may have landed.
|
||||
f.org.fireError = Object.assign(new Error('EIO: i/o error'), { code: 'EIO', revoked: true });
|
||||
await rejects(() => f.fire('helper'), 'outcome-unknown-revoked');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
await rejects(() => f.fire('helper'), 'revision-conflict');
|
||||
f.org.fireError = null;
|
||||
f.org.deleteError = Object.assign(new Error('refused'), { code: 'revision-conflict', revoked: true });
|
||||
assert.equal(f.remove('helper').code, 'revision-conflict-revoked');
|
||||
f.org.deleteError = Object.assign(new Error('EIO: i/o error'), { code: 'EIO', revoked: true });
|
||||
assert.equal(f.remove('helper').code, 'outcome-unknown-revoked');
|
||||
f.org.deleteError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
assert.equal(f.remove('helper').code, 'revision-conflict');
|
||||
});
|
||||
|
||||
test("row 239 T4b: an unmarked step B refusal keeps its code, and the notice says nothing of the tokens", async (t) => {
|
||||
const f = await setup(t);
|
||||
await f.approveHost('coder', 'web1');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
const out = f.fire('coder');
|
||||
await f.broker.grantsIdle();
|
||||
const [failed] = f.allowed('fire.failed');
|
||||
assert.deepEqual([failed.body.job, failed.body.code], [out.job, 'revision-conflict']);
|
||||
const [message] = f.inbox();
|
||||
assert.ok(message.includes('(revision-conflict). The instance is still firing.'), message);
|
||||
assert.ok(!message.includes('may already be revoked'), message);
|
||||
});
|
||||
@@ -13,4 +13,3 @@ export { configDir, businessFilePath, validateBusinessDocument, loadBusiness } f
|
||||
export { LEVELS, accessFilePath, checkAccess, loadAccess, covers } from "./access.mjs";
|
||||
export { projectFilePath, validateProjectDocument, loadProject } from "./project.mjs";
|
||||
export { systemVars, resolveInstance, classify } from "./resolve.mjs";
|
||||
export { loadSeatSets } from "./library-seats.mjs";
|
||||
@@ -144,8 +144,9 @@ function createdCredentials(old, instance, definition, args) {
|
||||
// checkRoutes, row 65). Row 194 (brief item 8, row 199 D13): revoke(business, instance, how,
|
||||
// services) revokes the instance's credentials for a fire or a delete and throws
|
||||
// credential-revoke-failed when it can't. Null in a writer with no bus (the CLI's), which revokes
|
||||
// nothing. Row 239 (#1745): writer is the organization writer, injected only by tests.
|
||||
export function instanceSettings({ dir, rolesDir, system, levels = () => null, revoke = null, writer = organizationWriter({ dir, rolesDir }) }) {
|
||||
// nothing.
|
||||
export function instanceSettings({ dir, rolesDir, system, levels = () => null, revoke = null }) {
|
||||
const writer = organizationWriter({ dir, rolesDir });
|
||||
|
||||
// The definitions in rolesDir and whether add and set accept each one.
|
||||
function definitions() {
|
||||
@@ -378,9 +379,8 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
|
||||
}
|
||||
}
|
||||
|
||||
// The checks every write runs, then the write, which runs beforeWrite (a fire's or a delete's
|
||||
// revoke) under its lock (row 239). A check that refuses revokes nothing, and a revoke that throws
|
||||
// writes nothing. beforeWrite must not write this business: the writer holds its lock.
|
||||
// The checks every write runs, then beforeWrite (a fire's or a delete's revoke), then the write. A
|
||||
// check that refuses revokes nothing, and a revoke that throws writes nothing.
|
||||
function commit(business, old, next, { revision, actor, context, beforeWrite = null }) {
|
||||
if (canonicalJson(next) === canonicalJson(old)) throw new OrganizationError("organization-no-change");
|
||||
|
||||
@@ -410,11 +410,12 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
|
||||
}
|
||||
}
|
||||
// Row 194 N1 (sage-1634): a delete writes on the revision its request read when it was raised,
|
||||
// so a write since then makes it stale. This early check refuses it without taking the lock. The
|
||||
// writer checks again under its lock, before the revoke (row 239), and that check is what keeps
|
||||
// a stale fire or delete from revoking.
|
||||
// so a write since then makes it stale. The revision is checked here, before the revoke, because
|
||||
// the writer's own check comes after it. A write between this read and the writer's lock still
|
||||
// revokes and then refuses (BUILD.md follow-up).
|
||||
if (writer.read(business).revision !== revision) throw new OrganizationError("revision-conflict", "organization revision changed; read again");
|
||||
const receipt = writer.write(business, next, { revision, actor, context, beforeWrite });
|
||||
if (beforeWrite) beforeWrite();
|
||||
const receipt = writer.write(business, next, { revision, actor, context });
|
||||
return { ...read(business, actor), transaction: receipt.transaction, restart: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// Case 2 design 5.1 (M7, M8, row 220): which seats work in a library business,
|
||||
// and which of those hold a creative definition. Read from the business files on
|
||||
// every call, so an edit takes effect without a restart. Fail closed: a business
|
||||
// file that can't be read or validated throws. So does anything at the businesses
|
||||
// path that can't be listed: a file, a dangling symlink, or a directory that can't
|
||||
// be read. Only a path with nothing at it (lstat ENOENT) holds no library business,
|
||||
// so both sets are empty. No caller turns a throw into an empty set.
|
||||
import { lstatSync, readdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BusinessError } from "./errors.mjs";
|
||||
import { configDir, loadBusiness } from "./business.mjs";
|
||||
import { CREATIVE_DEFINITIONS } from "./vocabulary.mjs";
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
|
||||
export function defaultRolesDir(env = process.env) {
|
||||
return resolve(env.MOSAIC_ROLES_DIR || join(REPO, "roles"));
|
||||
}
|
||||
|
||||
// Returns { librarySeats, creativeSeats }: Sets of seat names (the role keys of
|
||||
// each business). A seat in two businesses counts when either one puts it in a
|
||||
// set. creativeSeats is a subset of librarySeats.
|
||||
export function loadSeatSets({ dir = configDir(), rolesDir = defaultRolesDir() } = {}) {
|
||||
// lstat first: only nothing at the path is an absent directory. Anything else
|
||||
// there (a file, a dangling symlink, a directory) must list, or it throws.
|
||||
const businesses = join(dir, "businesses");
|
||||
try {
|
||||
lstatSync(businesses);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return { librarySeats: new Set(), creativeSeats: new Set() };
|
||||
throw new BusinessError(`cannot list businesses in ${dir}: ${err.message}`);
|
||||
}
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync(businesses).filter((name) => name.endsWith(".json"));
|
||||
} catch (err) {
|
||||
throw new BusinessError(`cannot list businesses in ${dir}: ${err.message}`);
|
||||
}
|
||||
const librarySeats = new Set();
|
||||
const creativeSeats = new Set();
|
||||
for (const name of names.sort()) {
|
||||
const business = loadBusiness(name.slice(0, -".json".length), { dir, rolesDir });
|
||||
if (!Object.values(business.projects).some((project) => project.kind === "library")) continue;
|
||||
for (const [seat, role] of Object.entries(business.roles)) {
|
||||
librarySeats.add(seat);
|
||||
if (CREATIVE_DEFINITIONS.includes(role.definition)) creativeSeats.add(seat);
|
||||
}
|
||||
}
|
||||
return { librarySeats, creativeSeats };
|
||||
}
|
||||
@@ -177,70 +177,55 @@ export function organizationWriter({ dir, rolesDir, barrier = () => {} }) {
|
||||
// Row 199 (#1697, design section 6): `detail` says what a credential verb did (verb, agent, kind,
|
||||
// date, reason), never a value; the other setters pass none. context, when given, is logged with
|
||||
// the change: row 194's hire and fire carry the chief's run and the decision there (D2.2).
|
||||
// Row 239 (#1745): beforeWrite (a fire's or a delete's credential revoke) runs under the lock,
|
||||
// after the revision check and the validations, so a refusal there revokes nothing. An error
|
||||
// after it returns carries revoked: true, the lock's release included (duckworth-0165 C2), and the
|
||||
// bus names it so the human reads that the tokens may already be revoked. beforeWrite must not
|
||||
// write this business itself: it would find its own lock and refuse organization-busy.
|
||||
function write(id, document, { revision, actor, detail, context, beforeWrite = null }) {
|
||||
function write(id, document, { revision, actor, detail, context }) {
|
||||
requireId(actor, "authenticated actor");
|
||||
requireId(id, "organization id");
|
||||
let revoked = false;
|
||||
try {
|
||||
return withLock(id, ({ home, parent }) => {
|
||||
recover(id);
|
||||
const file = businessFilePath(id, dir);
|
||||
const oldBytes = regular(file);
|
||||
const before = sha256(oldBytes);
|
||||
if (revision !== before) fail("revision-conflict", "organization revision changed; read again");
|
||||
const old = JSON.parse(oldBytes);
|
||||
// C2-04 M9: every organization setter preserves the whole project
|
||||
// mapping, including library playgrounds and hybrid universes.
|
||||
if (canonicalJson(document?.projects) !== canonicalJson(old.projects))
|
||||
fail('organization-projects-protected');
|
||||
validateBusinessDocument(document, file, { rolesDir });
|
||||
const bytes = JSON.stringify(document, null, 2) + "\n";
|
||||
// Check and log exactly what lands on disk, for every setter: a value JSON
|
||||
// can't carry (a Date, a Map) serializes as something else (row 131, sage-0314).
|
||||
const written = JSON.parse(bytes);
|
||||
if (canonicalJson(written.projects) !== canonicalJson(old.projects))
|
||||
fail('organization-projects-protected');
|
||||
validateBusinessDocument(written, file, { rolesDir });
|
||||
if (beforeWrite) {
|
||||
beforeWrite();
|
||||
revoked = true;
|
||||
}
|
||||
const after = sha256(bytes);
|
||||
const transaction = randomUUID();
|
||||
const tx = join(home, transaction);
|
||||
directory(tx);
|
||||
const previous = join(tx, "previous.json");
|
||||
durable(previous, oldBytes);
|
||||
const candidate = join(parent, "." + id + "." + transaction + ".tmp");
|
||||
durable(candidate, bytes);
|
||||
syncDir(tx);
|
||||
const event = {
|
||||
transaction, actor, ...(context === undefined ? {} : { context }), business: id, before, after,
|
||||
changes: changes(old, written), ...(detail === undefined ? {} : { detail }), at: new Date().toISOString(),
|
||||
};
|
||||
log(id, { ...event, state: "prepared" });
|
||||
syncDir(home);
|
||||
barrier("prepared");
|
||||
if (sha256(regular(file)) !== before) {
|
||||
log(id, { ...event, state: "aborted" });
|
||||
rmSync(candidate);
|
||||
fail("revision-conflict", "organization revision changed before publication");
|
||||
}
|
||||
renameSync(candidate, file);
|
||||
syncDir(parent);
|
||||
barrier("renamed");
|
||||
log(id, { ...event, state: "committed" });
|
||||
return { revision: after, transaction, previous };
|
||||
});
|
||||
} catch (e) {
|
||||
if (revoked && e instanceof Error) e.revoked = true;
|
||||
throw e;
|
||||
}
|
||||
return withLock(id, ({ home, parent }) => {
|
||||
recover(id);
|
||||
const file = businessFilePath(id, dir);
|
||||
const oldBytes = regular(file);
|
||||
const before = sha256(oldBytes);
|
||||
if (revision !== before) fail("revision-conflict", "organization revision changed; read again");
|
||||
const old = JSON.parse(oldBytes);
|
||||
// C2-04 M9: every organization setter preserves the whole project
|
||||
// mapping, including library playgrounds and hybrid universes.
|
||||
if (canonicalJson(document?.projects) !== canonicalJson(old.projects))
|
||||
fail('organization-projects-protected');
|
||||
validateBusinessDocument(document, file, { rolesDir });
|
||||
const bytes = JSON.stringify(document, null, 2) + "\n";
|
||||
// Check and log exactly what lands on disk, for every setter: a value JSON
|
||||
// can't carry (a Date, a Map) serializes as something else (row 131, sage-0314).
|
||||
const written = JSON.parse(bytes);
|
||||
if (canonicalJson(written.projects) !== canonicalJson(old.projects))
|
||||
fail('organization-projects-protected');
|
||||
validateBusinessDocument(written, file, { rolesDir });
|
||||
const after = sha256(bytes);
|
||||
const transaction = randomUUID();
|
||||
const tx = join(home, transaction);
|
||||
directory(tx);
|
||||
const previous = join(tx, "previous.json");
|
||||
durable(previous, oldBytes);
|
||||
const candidate = join(parent, "." + id + "." + transaction + ".tmp");
|
||||
durable(candidate, bytes);
|
||||
syncDir(tx);
|
||||
const event = {
|
||||
transaction, actor, ...(context === undefined ? {} : { context }), business: id, before, after,
|
||||
changes: changes(old, written), ...(detail === undefined ? {} : { detail }), at: new Date().toISOString(),
|
||||
};
|
||||
log(id, { ...event, state: "prepared" });
|
||||
syncDir(home);
|
||||
barrier("prepared");
|
||||
if (sha256(regular(file)) !== before) {
|
||||
log(id, { ...event, state: "aborted" });
|
||||
rmSync(candidate);
|
||||
fail("revision-conflict", "organization revision changed before publication");
|
||||
}
|
||||
renameSync(candidate, file);
|
||||
syncDir(parent);
|
||||
barrier("renamed");
|
||||
log(id, { ...event, state: "committed" });
|
||||
return { revision: after, transaction, previous };
|
||||
});
|
||||
}
|
||||
|
||||
// Row 199: a receipt with no document change, for a credential verb that changes only the agents'
|
||||
|
||||
@@ -27,10 +27,6 @@ test('C2-04: library mappings load without roots, remain frozen, and retain mapp
|
||||
assert.deepEqual(validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }).projects.books.universes.harbor.playground, ['Elsewhere/Playground/']);
|
||||
doc.projects.books.universes.harbor.playground = ['Second World/Playground/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-playground-other-universe/);
|
||||
// C2-01d: a supporting prefix inside another universe's book refuses with its own code.
|
||||
doc.projects.books = mappingEntry();
|
||||
doc.projects.books.universes.harbor.supporting = ['Second World/Notes/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-supporting-other-universe/);
|
||||
doc.projects.books = { ...mappingEntry(), root: '/forbidden' };
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-mapping-invalid/);
|
||||
});
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// Case 2 M7/M8 (design 5.1, row 220): loadSeatSets reads every business file in
|
||||
// the config directory. A seat is a library seat when a business with a library
|
||||
// project names it; a creative seat is a library seat whose definition is creative.
|
||||
// Any file it can't read or validate throws, and so does a businesses path that is
|
||||
// not a directory. An absent businesses directory gives empty sets.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync, chmodSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { loadSeatSets } from "../src/library-seats.mjs";
|
||||
import { BusinessError } from "../src/index.mjs";
|
||||
import { businessDoc, writeJson, REPO_ROLES } from "./helpers.mjs";
|
||||
import { mappingEntry } from "../../library/tests/helpers.mjs";
|
||||
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-seats-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const dir = join(root, "config");
|
||||
mkdirSync(join(dir, "businesses"), { recursive: true });
|
||||
return { root, dir };
|
||||
}
|
||||
|
||||
const sets = (dir) => loadSeatSets({ dir, rolesDir: REPO_ROLES });
|
||||
|
||||
test("M7/M8: library seats come from library businesses, creative seats from their creative definitions", (t) => {
|
||||
const { root, dir } = fixture(t);
|
||||
const doc = businessDoc(root, "acme");
|
||||
doc.projects = { books: mappingEntry(), ...doc.projects };
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
writeJson(join(dir, "businesses", "acme.json"), doc);
|
||||
const found = sets(dir);
|
||||
assert.deepEqual([...found.librarySeats].sort(), ["bean", "chief", "coder", "cto", "pm", "reviewer"]);
|
||||
assert.deepEqual([...found.creativeSeats], ["bean"]);
|
||||
assert.ok([...found.creativeSeats].every((seat) => found.librarySeats.has(seat)));
|
||||
});
|
||||
|
||||
test("M8: a business with no library project puts no seat in either set", (t) => {
|
||||
const { root, dir } = fixture(t);
|
||||
const doc = businessDoc(root, "other");
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
writeJson(join(dir, "businesses", "other.json"), doc);
|
||||
const found = sets(dir);
|
||||
assert.equal(found.librarySeats.size, 0);
|
||||
assert.equal(found.creativeSeats.size, 0);
|
||||
});
|
||||
|
||||
test("M7/M8: a business file that is not valid JSON throws; no empty set", (t) => {
|
||||
const { root, dir } = fixture(t);
|
||||
writeJson(join(dir, "businesses", "acme.json"), businessDoc(root, "acme"));
|
||||
const broken = join(dir, "businesses", "broken.json");
|
||||
writeFileSync(broken, "{");
|
||||
chmodSync(broken, 0o600);
|
||||
assert.throws(() => sets(dir), BusinessError);
|
||||
});
|
||||
|
||||
test("M7/M8: an absent businesses directory gives empty sets", (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-seats-none-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const found = sets(join(root, "config"));
|
||||
assert.equal(found.librarySeats.size, 0);
|
||||
assert.equal(found.creativeSeats.size, 0);
|
||||
});
|
||||
|
||||
test("M7/M8: a businesses path that is not a directory throws; no empty set", (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-seats-file-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const dir = join(root, "config");
|
||||
mkdirSync(dir);
|
||||
writeFileSync(join(dir, "businesses"), "not a directory");
|
||||
assert.throws(() => sets(dir), (err) => err instanceof BusinessError && /cannot list businesses/.test(err.message));
|
||||
});
|
||||
|
||||
test("M7/M8: a businesses symlink to a missing target throws; no empty set", (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-seats-dangling-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const dir = join(root, "config");
|
||||
mkdirSync(dir);
|
||||
symlinkSync(join(root, "missing"), join(dir, "businesses"));
|
||||
assert.throws(() => sets(dir), (err) => err instanceof BusinessError && /cannot list businesses/.test(err.message));
|
||||
});
|
||||
|
||||
test("M7/M8: a config directory under a regular file throws (lstat fails with ENOTDIR); no empty set", (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-seats-notdir-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const dir = join(root, "config");
|
||||
writeFileSync(dir, "not a directory");
|
||||
assert.throws(() => sets(dir), (err) => err instanceof BusinessError && /cannot list businesses/.test(err.message));
|
||||
});
|
||||
|
||||
test("M7/M8: a business file that can't be read throws; no empty set", (t) => {
|
||||
const { root, dir } = fixture(t);
|
||||
writeJson(join(dir, "businesses", "acme.json"), businessDoc(root, "acme"));
|
||||
const file = join(dir, "businesses", "acme.json");
|
||||
chmodSync(file, 0o000);
|
||||
t.after(() => {
|
||||
// The fixture's rmSync hook can run first, so the file may already be gone.
|
||||
try {
|
||||
chmodSync(file, 0o600);
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") throw err;
|
||||
}
|
||||
});
|
||||
// Root reads a mode 000 file, so the check cannot hold there.
|
||||
if (process.getuid?.() === 0) return t.skip("running as root: mode 000 files stay readable");
|
||||
assert.throws(() => sets(dir), (err) => err instanceof BusinessError && /must be a regular, non-symbolic-link file/.test(err.message));
|
||||
});
|
||||
@@ -4,10 +4,9 @@
|
||||
import test, { after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { readFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { instanceSettings } from "../src/instance-settings.mjs";
|
||||
import { OrganizationError, organizationWriter } from "../src/organization.mjs";
|
||||
import { CREATIVE_DEFINITIONS } from "../src/vocabulary.mjs";
|
||||
import { businessDoc, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
@@ -37,7 +36,7 @@ function fixture(mutate) {
|
||||
const log = () => readFileSync(settings.writer.logFile("acme"), "utf8").trim().split("\n").map(line => JSON.parse(line));
|
||||
const onDisk = () => JSON.parse(readFileSync(file, "utf8"));
|
||||
const revision = () => settings.read("acme").revision;
|
||||
return { root, dir, rolesDir, file, settings, validate, log, onDisk, revision };
|
||||
return { root, file, settings, validate, log, onDisk, revision };
|
||||
}
|
||||
|
||||
// Each refusal: its code (or the validator's BusinessError), and the file's bytes unchanged.
|
||||
@@ -245,73 +244,3 @@ test("row 256 checkDelete passes the human's creative delete and refuses another
|
||||
assert.throws(() => f.settings.checkFire("acme", "bean"), e => e.code === "organization-definition-refused");
|
||||
assert.equal(readFileSync(f.file, "utf8"), bytes);
|
||||
});
|
||||
|
||||
// Row 239 (#1745): a fire's or a delete's revoke runs inside the writer, under its lock, after the
|
||||
// revision check. revoking(f, writer, fail) builds settings on f's files with a spy revoke and that writer.
|
||||
function revoking(f, writer, fail = null) {
|
||||
const calls = [];
|
||||
const revoke = (business, agent, how, services) => {
|
||||
calls.push([business, agent, how, services]);
|
||||
if (fail) throw fail;
|
||||
};
|
||||
const settings = instanceSettings({ dir: f.dir, rolesDir: f.rolesDir, system: systemFor(f.root), revoke, ...(writer ? { writer } : {}) });
|
||||
return { settings, calls };
|
||||
}
|
||||
const receipts = f => (existsSync(f.settings.writer.logFile("acme")) ? f.log() : []);
|
||||
// A hand edit: the same document in other bytes, so the revision changes and the file still validates.
|
||||
const handEdit = f => {
|
||||
const bytes = JSON.stringify(f.onDisk()) + "\n";
|
||||
writeFileSync(f.file, bytes);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
test("row 239 T1 a fire on a revision that goes stale before the writer's lock refuses there and revokes nothing", () => {
|
||||
const f = fixture();
|
||||
const real = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir });
|
||||
let edited = null;
|
||||
// The edit lands after commit()'s early check and before the writer takes its lock.
|
||||
const writer = { ...real, write: (id, document, options) => { edited = handEdit(f); return real.write(id, document, options); } };
|
||||
const { settings, calls } = revoking(f, writer);
|
||||
const before = receipts(f).length;
|
||||
assert.throws(() => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT }),
|
||||
e => e.code === "revision-conflict" && e.revoked === undefined);
|
||||
assert.deepEqual(calls, []);
|
||||
assert.equal(readFileSync(f.file, "utf8"), edited);
|
||||
assert.equal(receipts(f).length, before);
|
||||
});
|
||||
|
||||
test("row 239 T2 a write after the revoke refused before publication carries revoked, and the file keeps the edit", () => {
|
||||
const f = fixture();
|
||||
let edited = null;
|
||||
const writer = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: stage => { if (stage === "prepared") edited = handEdit(f); } });
|
||||
const { settings, calls } = revoking(f, writer);
|
||||
assert.throws(() => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT }),
|
||||
e => e.code === "revision-conflict" && e.revoked === true);
|
||||
assert.deepEqual(calls, [["acme", "sec-1", "fire", []]]);
|
||||
assert.equal(readFileSync(f.file, "utf8"), edited);
|
||||
assert.equal(receipts(f).at(-1).state, "aborted");
|
||||
});
|
||||
|
||||
test("row 239 T3 any error after the revoke carries revoked, for a fire and a delete; a revoke that throws carries nothing", () => {
|
||||
for (const [how, act] of [
|
||||
["fire", settings => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT })],
|
||||
["delete", (settings, f) => settings.set("acme", { revision: f.revision(), instance: "sec-1", op: "delete" }, "jason")],
|
||||
]) {
|
||||
const f = fixture();
|
||||
const bytes = readFileSync(f.file, "utf8");
|
||||
const writer = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: stage => { if (stage === "prepared") throw new Error("stopped at prepared"); } });
|
||||
const { settings, calls } = revoking(f, writer);
|
||||
assert.throws(() => act(settings, f), e => e.message === "stopped at prepared" && e.revoked === true, how);
|
||||
assert.equal(calls.length, 1, how);
|
||||
assert.equal(calls[0][2], how);
|
||||
assert.equal(readFileSync(f.file, "utf8"), bytes, how);
|
||||
|
||||
const g = fixture();
|
||||
const gBytes = readFileSync(g.file, "utf8");
|
||||
const failing = revoking(g, null, new OrganizationError("credential-revoke-failed"));
|
||||
assert.throws(() => act(failing.settings, g), e => e.code === "credential-revoke-failed" && e.revoked === undefined, how);
|
||||
assert.equal(failing.calls.length, 1, how);
|
||||
assert.equal(readFileSync(g.file, "utf8"), gBytes, how);
|
||||
assert.deepEqual(receipts(g), [], how);
|
||||
}
|
||||
});
|
||||
@@ -55,12 +55,6 @@ export const REASONS = {
|
||||
"credential-revoke-failed": "a credential couldn't be revoked, so the business file is unchanged; tokens already revoked " +
|
||||
"stay revoked and the instance can't use them; clear what else the agent's store directory holds and run the same " +
|
||||
"command again, which is safe",
|
||||
// Row 239 (#1745): a fire's or a delete's revoke runs under the writer's lock; these follow one that ran.
|
||||
"revision-conflict-revoked": "the business file changed after the agent's tokens were revoked and before the write, so " +
|
||||
"the file is unchanged; the tokens may already be revoked and the instance can't use them; read it again with " +
|
||||
"`mosaic business instances` and run the same command again, which is safe",
|
||||
"outcome-unknown-revoked": "the agent's tokens may already be revoked and the write's outcome is unknown; read the " +
|
||||
"business file again with `mosaic business instances` before trying again",
|
||||
"capacity-full": "a launch limit is reached; `mosaic limits --business BUSINESS` shows them",
|
||||
"limit-reached": "a launch limit is reached; `mosaic limits --business BUSINESS` shows them",
|
||||
"unknown-role": "the business has no such instance",
|
||||
|
||||
@@ -42,7 +42,6 @@ const EXIT = {
|
||||
"tester-off": 3,
|
||||
"tester-refused": 3,
|
||||
"outcome-unknown": 1,
|
||||
"outcome-unknown-revoked": 1,
|
||||
"response-too-large": 1,
|
||||
"invalid-response": 1,
|
||||
};
|
||||
|
||||
@@ -333,51 +333,3 @@ test("row 240 pins and usage: the shipped pins are r6's, the script reads the pi
|
||||
assert.deepEqual([u.status, u.stderr], [2, "hostcheck: refusing: run it as root\n"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("Vikunja 614 check: item 5 accepts dpkg's bare octal mode and leading-zero equivalents", (t) => {
|
||||
for (const mode of ["750", "0750"]) {
|
||||
const h = host(t);
|
||||
put(join(h.hc, "statoverride"), `root mosaic ${mode} /usr/bin/bwrap\n`);
|
||||
const r = run(h, "--check");
|
||||
assert.equal(r.status, 0, r.out + r.err);
|
||||
assert.deepEqual(fails(r), []);
|
||||
assert.ok(r.lines.some((line) => line.startsWith("ok 5 ")), r.out);
|
||||
}
|
||||
});
|
||||
|
||||
test("Vikunja 614 enforce: dpkg's bare 750 leaves bwrap open and writes no DRIFT", (t) => {
|
||||
const h = host(t);
|
||||
put(join(h.hc, "statoverride"), "root mosaic 750 /usr/bin/bwrap\n");
|
||||
const r = run(h, "--enforce");
|
||||
assert.equal(r.status, 0, r.out + r.err);
|
||||
assert.deepEqual(fails(r), []);
|
||||
assert.equal(r.lines.at(-1), "hostcheck: all seven hold");
|
||||
assert.deepEqual([calls(h, "apparmor_parser"), calls(h, "chown"), calls(h, "chmod")], [[], [], []]);
|
||||
assert.equal(existsSync(drift(h)), false);
|
||||
assert.equal(existsSync(hlog(h)), false);
|
||||
});
|
||||
|
||||
test("Vikunja 614 check: item 5 refuses other modes, owners, groups, paths and malformed overrides", (t) => {
|
||||
const overrides = [
|
||||
...["755", "0755", "4750", "75", "7500", "075", "00750", "000750", "0780", "8", "0o750", "+750", "-750", "7x0", "750+0", ""].map((mode) => `root mosaic ${mode} /usr/bin/bwrap\n`),
|
||||
"root mosaic 750 /usr/bin/bwrap\n",
|
||||
"root\tmosaic 750 /usr/bin/bwrap\n",
|
||||
"mosaic mosaic 750 /usr/bin/bwrap\n",
|
||||
"root root 750 /usr/bin/bwrap\n",
|
||||
"root mosaic 750 /usr/bin/other\n",
|
||||
"root mosaic 750 /usr/bin/bwrap extra\n",
|
||||
"root mosaic 750\n",
|
||||
"root mosaic 750 /usr/bin/bwrap\nroot mosaic 750 /usr/bin/bwrap\n",
|
||||
"\n",
|
||||
"",
|
||||
null,
|
||||
];
|
||||
for (const override of overrides) {
|
||||
const h = host(t);
|
||||
if (override === null) rmSync(join(h.hc, "statoverride"));
|
||||
else put(join(h.hc, "statoverride"), override);
|
||||
const r = run(h, "--check");
|
||||
assert.equal(r.status, 1, `${JSON.stringify(override)}: ${r.out}${r.err}`);
|
||||
assert.deepEqual(fails(r).map((line) => line.split(" ")[1]), ["5"], r.out);
|
||||
}
|
||||
});
|
||||
@@ -1,419 +0,0 @@
|
||||
// Case 2 row C2-13b (#1727): scripts/case2-install.sh installs a hub as <user>, and
|
||||
// scripts/case2-precheck.sh's library checks and --library-status mode. Brief:
|
||||
// docs/plans/2026-10-20_case2-rows.md "C2-13b". Every case runs in a scratch home with its own
|
||||
// MOSAIC_CONFIG, a stub stack-copy and fake getent, sudo, id, node, bwrap and systemctl. The library
|
||||
// sync runs in-process against a local bare repo, as the bus would. Nothing here reads or writes
|
||||
// ~/.mosaic-dev, ~/.config/mosaic-dev, /etc or a network host, and no fixture value is a credential.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
|
||||
import { BASE, MARKER, git, mappingEntry, repo } from "../../library/tests/helpers.mjs";
|
||||
import { ensureMirror, mirrorPath, sync } from "../../library/src/index.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { SAFE } from "../../project/src/git.mjs";
|
||||
import { tmp } from "./helpers.mjs";
|
||||
|
||||
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const INSTALL = join(REPO, "scripts", "case2-install.sh");
|
||||
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
const COMMIT = "c".repeat(40);
|
||||
const UID = process.getuid();
|
||||
const GID = process.getgid();
|
||||
// Made-up values; no output may hold them.
|
||||
const FIXTURE_KEY = "fixture-zai-key-8d41a7";
|
||||
const PERSONA_TEXT = { bean: "Bean is warm and brief. PERSONA-TEXT-bean-61f0\n", chet: "Chet is dry and exact. PERSONA-TEXT-chet-2b9e\n" };
|
||||
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
||||
const mode = (p) => statSync(p).mode & 0o777;
|
||||
|
||||
function script(file, text) {
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
|
||||
chmodSync(file, 0o755);
|
||||
return file;
|
||||
}
|
||||
|
||||
// A scratch <user>: a home, an input business file, a persona staging dir and fakes.
|
||||
function world(t, { profile = "library" } = {}) {
|
||||
const root = tmp(t, "case2-install-");
|
||||
const home = join(root, "home");
|
||||
const configDir = join(home, ".config", "mosaic-dev");
|
||||
// claude-session.env is in place before the install, as 2d would leave it on a re-run.
|
||||
const secrets = join(configDir, "secrets");
|
||||
mkdirSync(secrets, { recursive: true, mode: 0o700 });
|
||||
chmodSync(configDir, 0o700);
|
||||
writeFileSync(join(secrets, "claude-session.env"), `ZAI_API_KEY=${FIXTURE_KEY}\n`, { mode: 0o600 });
|
||||
const doc = businessDoc(root);
|
||||
for (const r of Object.values(doc.roles)) r.vars = { ...r.vars, harness: "pi" };
|
||||
if (profile === "library") {
|
||||
doc.projects.books = { ...mappingEntry(), user: "writer" };
|
||||
doc.roles.bean = { definition: "companion", vars: { harness: "pi" } };
|
||||
doc.roles.chet = { definition: "companion", vars: { harness: "pi" } };
|
||||
} else doc.vars["bus.tester"] = true;
|
||||
const input = writeJson(join(root, "input", "acme.json"), doc);
|
||||
const staging = join(root, "staging");
|
||||
mkdirSync(staging, { mode: 0o700 });
|
||||
const personas = [];
|
||||
for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) {
|
||||
writeFileSync(join(staging, `${instance}.md`), PERSONA_TEXT[instance], { mode: 0o600 });
|
||||
personas.push({ instance, name, bytes: Buffer.byteLength(PERSONA_TEXT[instance]), sha256: sha256(PERSONA_TEXT[instance]) });
|
||||
}
|
||||
writeFileSync(join(staging, "manifest.json"), `${JSON.stringify({ version: 1, personas }, null, 2)}\n`, { mode: 0o600 });
|
||||
const bin = join(root, "bin");
|
||||
const log = join(root, "calls.log");
|
||||
script(join(bin, "stack-copy"), `echo "stack-copy $*" >>'${log}'
|
||||
[ "$1" = copy ] && [ "$3" = --copies ] || exit 4
|
||||
mkdir -p "$4/$2/scripts" && : >"$4/$2/scripts/mosaic" && echo "copied: $4/$2"`);
|
||||
script(join(bin, "systemctl"), `echo "systemctl $*" >>'${log}'`);
|
||||
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
|
||||
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
|
||||
script(join(bin, "sudo"), "exit 1");
|
||||
script(join(bin, "id"), `if [ "$1" = -G ]; then echo '${GID}'; elif [ "$1" = -u ] && [ -n "\${CASE2_TEST_UID:-}" ]; then echo "$CASE2_TEST_UID"; else exec /usr/bin/id "$@"; fi`);
|
||||
script(join(bin, "node"), `if [ "$1" = --version ]; then echo v24.3.0; exit 0; fi\nexec '${process.execPath}' "$@"`);
|
||||
script(join(bin, "bwrap"), "exit 0");
|
||||
const units = join(root, "units");
|
||||
mkdirSync(units);
|
||||
writeFileSync(join(units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port 443 --business acme\n");
|
||||
writeFileSync(join(units, "[email protected]"), "[Service]\nEnvironment=PATH=/usr/bin\n");
|
||||
const sys = join(root, "sys");
|
||||
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
|
||||
mkdirSync(join(sys, "kernel"), { recursive: true });
|
||||
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
|
||||
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
|
||||
const notify = join(home, ".mosaic-dev", "notify", "acme");
|
||||
mkdirSync(notify, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(notify, "notify.json"), '{"notifyVersion":1,"binding":null}\n', { mode: 0o600 });
|
||||
return {
|
||||
root, home, configDir, secrets, input, staging, bin, log, units, sys, doc,
|
||||
config: join(configDir, "config.json"),
|
||||
business: join(configDir, "businesses", "acme.json"),
|
||||
personaDir: join(configDir, "personas", "acme"),
|
||||
slot: join(secrets, "acme"),
|
||||
dataRoot: join(home, ".mosaic-dev"),
|
||||
};
|
||||
}
|
||||
|
||||
function env(x, extra = {}) {
|
||||
return { PATH: `${x.bin}:/usr/bin:/bin`, HOME: x.home, MOSAIC_CONFIG: x.config, CASE2_STACK_COPY: join(x.bin, "stack-copy"), ...extra };
|
||||
}
|
||||
|
||||
function clean(out) {
|
||||
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the output holds the fixture key");
|
||||
assert.doesNotMatch(out, /PERSONA-TEXT/, "the output holds persona text");
|
||||
assert.doesNotMatch(out, new RegExp(MARKER), "the output holds her text");
|
||||
}
|
||||
|
||||
function install(x, { profile = "library", args = null, extra = {} } = {}) {
|
||||
const argv = args ?? ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--profile", profile,
|
||||
...(profile === "library" ? ["--personas", x.staging] : [])];
|
||||
const r = spawnSync(INSTALL, argv, { encoding: "utf8", timeout: 120000, env: env(x, extra) });
|
||||
const out = r.stdout + r.stderr;
|
||||
clean(out);
|
||||
return { code: r.status, out };
|
||||
}
|
||||
|
||||
function precheck(x, { args = ["--business", "acme", "--profile", "library"] } = {}) {
|
||||
const r = spawnSync(PRECHECK, args, {
|
||||
encoding: "utf8",
|
||||
timeout: 60000,
|
||||
env: env(x, { CASE2_UNIT_DIR: x.units, CASE2_PROC_SYS: x.sys, CASE2_GETENT: join(x.bin, "getent") }),
|
||||
});
|
||||
const out = r.stdout + r.stderr;
|
||||
clean(out);
|
||||
return { code: r.status, out };
|
||||
}
|
||||
const status = (x) => precheck(x, { args: ["--business", "acme", "--library-status"] });
|
||||
|
||||
function refused(r, pattern) {
|
||||
assert.equal(r.code, 1, r.out);
|
||||
assert.match(r.out, pattern);
|
||||
}
|
||||
|
||||
const calls = (x) => (existsSync(x.log) ? readFileSync(x.log, "utf8") : "");
|
||||
|
||||
// ---- case2-install.sh ----
|
||||
|
||||
test("the library install writes the business file, personas and slot dir, makes the copy and starts nothing", (t) => {
|
||||
const x = world(t);
|
||||
const r = install(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.equal(mode(x.business), 0o600);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) {
|
||||
assert.equal(doc.roles[instance].vars["persona.name"], name);
|
||||
assert.equal(doc.roles[instance].vars["persona.sha256"], sha256(PERSONA_TEXT[instance]));
|
||||
assert.equal(doc.roles[instance].vars.harness, "pi");
|
||||
const file = join(x.personaDir, `${instance}.md`);
|
||||
assert.equal(readFileSync(file, "utf8"), PERSONA_TEXT[instance]);
|
||||
assert.equal(mode(file), 0o600);
|
||||
}
|
||||
assert.equal(mode(join(x.configDir, "personas")), 0o700);
|
||||
assert.equal(mode(x.personaDir), 0o700);
|
||||
assert.equal(mode(x.slot), 0o700);
|
||||
assert.deepEqual(readdirSync(x.slot), []);
|
||||
assert.equal(calls(x), `stack-copy copy ${COMMIT} --copies ${join(x.home, ".local", "share", "mosaic-stack", "copies")}\n`);
|
||||
assert.doesNotMatch(r.out, /release/);
|
||||
assert.match(r.out, /business acme validates/);
|
||||
assert.match(r.out, /--library-status/);
|
||||
assert.ok(existsSync(x.config));
|
||||
});
|
||||
|
||||
test("a library re-run passes and leaves every file as it was", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const before = readFileSync(x.business);
|
||||
const r = install(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, /already matches/);
|
||||
assert.match(r.out, /personas already installed/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
test("a re-run refuses business-file-differs and leaves the file unchanged", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
doc.vars["tracker.pollSeconds"] = 120;
|
||||
writeJson(x.business, doc);
|
||||
const before = readFileSync(x.business);
|
||||
refused(install(x), /business-file-differs/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
test("a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
rmSync(x.business);
|
||||
writeFileSync(join(x.personaDir, "bean.md"), "Someone else. PERSONA-TEXT-other\n");
|
||||
refused(install(x), /persona-differs/);
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the factory install writes no library mapping, persona or slot dir", (t) => {
|
||||
const x = world(t, { profile: "factory" });
|
||||
const r = install(x, { profile: "factory" });
|
||||
assert.equal(r.code, 0, r.out);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
assert.equal(Object.values(doc.projects).some((p) => p.kind === "library"), false);
|
||||
assert.equal(existsSync(join(x.configDir, "personas")), false);
|
||||
assert.equal(existsSync(x.slot), false);
|
||||
assert.doesNotMatch(r.out, /--library-status/);
|
||||
});
|
||||
|
||||
test("the factory install refuses a library project and bus.tester off, before anything is written", (t) => {
|
||||
const x = world(t, { profile: "factory" });
|
||||
const doc = JSON.parse(readFileSync(x.input, "utf8"));
|
||||
doc.projects.books = { ...mappingEntry(), user: "writer" };
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x, { profile: "factory" }), /profile-library-business/);
|
||||
delete doc.projects.books;
|
||||
doc.vars["bus.tester"] = false;
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x, { profile: "factory" }), /bus-tester-off/);
|
||||
assert.equal(calls(x), "");
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the library install refuses bus.tester on and a persona var in the input", (t) => {
|
||||
const x = world(t);
|
||||
const doc = JSON.parse(readFileSync(x.input, "utf8"));
|
||||
doc.vars["bus.tester"] = true;
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x), /bus-tester-on/);
|
||||
doc.vars["bus.tester"] = false;
|
||||
doc.roles.bean.vars["persona.name"] = "Bean";
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x), /persona-var-input/);
|
||||
assert.equal(calls(x), "");
|
||||
});
|
||||
|
||||
test("the install refuses root, a business id mismatch, another --home and a config dir with group bits", (t) => {
|
||||
const x = world(t);
|
||||
refused(install(x, { extra: { CASE2_TEST_UID: "0" } }), /run-as-root/);
|
||||
refused(install(x, { args: ["--business", "other", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging] }), /business-id-mismatch/);
|
||||
refused(install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging, "--home", x.root] }), /home-mismatch/);
|
||||
chmodSync(x.configDir, 0o750);
|
||||
refused(install(x), /config-dir-mode/);
|
||||
assert.equal(calls(x), "");
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the library install needs --personas", (t) => {
|
||||
const x = world(t);
|
||||
const r = install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT] });
|
||||
assert.equal(r.code, 4, r.out);
|
||||
assert.match(r.out, /needs --personas/);
|
||||
});
|
||||
|
||||
test("a validate refusal removes the business file this run wrote", (t) => {
|
||||
const x = world(t);
|
||||
unlinkSync(x.doc.roles.pm.credentials.gitea.file);
|
||||
const r = install(x);
|
||||
refused(r, /validate: .*the business file this run wrote is removed/);
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const before = readFileSync(x.business);
|
||||
unlinkSync(x.doc.roles.pm.credentials.gitea.file);
|
||||
const r = install(x);
|
||||
refused(r, /validate: /);
|
||||
assert.doesNotMatch(r.out, /is removed/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
// ---- case2-precheck.sh, the library profile (D6a) ----
|
||||
|
||||
test("a clean library host passes the precheck with no mirror yet", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const r = precheck(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, /ok {6}business file .* with no group or other bit/);
|
||||
assert.match(r.out, /ok {6}no history for acme yet/);
|
||||
assert.match(r.out, new RegExp(`ok {6}persona bean sha256 ${sha256(PERSONA_TEXT.bean)}`));
|
||||
assert.match(r.out, new RegExp(`ok {6}persona chet sha256 ${sha256(PERSONA_TEXT.chet)}`));
|
||||
assert.match(r.out, /ok {6}no Discord binding/);
|
||||
assert.equal(existsSync(mirrorPath({ business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot })), false);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona file at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(join(x.personaDir, "bean.md"), 0o644);
|
||||
refused(precheck(x), /REFUSED persona-mode: .*bean\.md must be .* 0600 file \(mode 644\)/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona whose digest isn't persona.sha256", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
writeFileSync(join(x.personaDir, "chet.md"), "Not Chet. PERSONA-TEXT-other\n");
|
||||
refused(precheck(x), /REFUSED persona-mismatch: persona chet has sha256 [0-9a-f]{64}, and persona.sha256 is /);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona dir with group bits", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(x.personaDir, 0o750);
|
||||
refused(precheck(x), /REFUSED persona-mode: .*personas\/acme must be .* 0700 directory/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a business file at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(x.business, 0o644);
|
||||
refused(precheck(x), /REFUSED business-file-mode: .*acme\.json .*\(mode 644\)/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a changes.jsonl at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const history = join(x.configDir, "businesses", ".history", "acme");
|
||||
mkdirSync(history, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(history, "changes.jsonl"), "", { mode: 0o644 });
|
||||
chmodSync(join(history, "changes.jsonl"), 0o644);
|
||||
refused(precheck(x), /REFUSED history-mode: .*changes\.jsonl must be .* 0600 file \(mode 644\)/);
|
||||
chmodSync(join(history, "changes.jsonl"), 0o600);
|
||||
assert.equal(precheck(x).code, 0);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a Discord binding", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
mkdirSync(join(x.dataRoot, "discord"), { recursive: true });
|
||||
writeFileSync(join(x.dataRoot, "discord", "her.json"), "{}\n", { mode: 0o600 });
|
||||
refused(precheck(x), /REFUSED discord-binding: .*her\.json exists/);
|
||||
});
|
||||
|
||||
// ---- case2-precheck.sh --library-status (D6b) ----
|
||||
|
||||
// The context the bus builds for books (runtime.mjs:161-164), and a sync the way the bus's timer
|
||||
// runs it, against a local bare clone through the command parameter.
|
||||
function library(x) {
|
||||
const context = { business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot };
|
||||
mkdirSync(x.dataRoot, { recursive: true, mode: 0o700 });
|
||||
chmodSync(x.dataRoot, 0o700);
|
||||
const run = (bare, slot) => sync(context, {
|
||||
slot,
|
||||
command: (kind, url, { mirror, branch }) => ({
|
||||
argv: [...SAFE, "-c", "protocol.file.allow=always", "-c", "core.createObject=rename", "-C", mirror, "fetch", "--no-tags",
|
||||
"--no-write-fetch-head", "--end-of-options", bare, `+refs/heads/${branch}:refs/remotes/library/${branch}`],
|
||||
env: { ...gitEnv(), LC_ALL: "C", GIT_CEILING_DIRECTORIES: dirname(mirror) },
|
||||
}),
|
||||
});
|
||||
return { context, run };
|
||||
}
|
||||
|
||||
test("--library-status refuses library-mirror-missing before the bus has made the mirror", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
refused(status(x), /REFUSED library-mirror-missing: library books has no mirror at .*the bus makes it when it starts/);
|
||||
});
|
||||
|
||||
test("--library-status refuses library-never-synced on a fresh mirror", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
await ensureMirror(library(x).context);
|
||||
const r = status(x);
|
||||
refused(r, /REFUSED library-never-synced: library books has no fetch yet/);
|
||||
assert.doesNotMatch(r.out, /library-last-error/);
|
||||
});
|
||||
|
||||
test("--library-status names a recorded lastError before the null lastFetch it leaves (N3)", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const lib = library(x);
|
||||
await lib.run("/nonexistent", join(x.slot, "library-gitea"));
|
||||
const r = status(x);
|
||||
refused(r, /REFUSED library-last-error: library books: library-credential-missing at \d{4}-\d\d-\d\dT/);
|
||||
assert.doesNotMatch(r.out, /library-never-synced/);
|
||||
});
|
||||
|
||||
test("--library-status prints each R-problem's path as an escaped JSON string, then the count, and passes", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const odd = "OddName\u0085.md";
|
||||
const f = repo(t, { ...BASE, [odd]: `${MARKER}\n` });
|
||||
const bare = join(f.root, "remote.git");
|
||||
git(f.root, "clone", "-q", "--bare", "--no-local", f.repo, bare);
|
||||
writeFileSync(join(x.slot, "library-gitea"), "placeholder-not-a-token\n", { mode: 0o600 });
|
||||
const { state } = await library(x).run(bare, join(x.slot, "library-gitea"));
|
||||
assert.equal(state.lastError, null);
|
||||
const r = status(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, new RegExp(`ok {6}library books last fetched \\S+, serving ${f.commit}`));
|
||||
assert.match(r.out, /^problem {3}R2 "Inbox Note\.md"$/m);
|
||||
assert.match(r.out, /^problem {3}R\d+ "Odd\\u202eName\\u0085\.md"$/m);
|
||||
assert.doesNotMatch(r.out, /[\u0085]/);
|
||||
const lines = r.out.split("\n").filter((l) => l.startsWith("problem "));
|
||||
assert.equal(lines.length, state.problems.length);
|
||||
assert.match(r.out, new RegExp(`^problems ${state.problems.length}$`, "m"));
|
||||
assert.match(r.out, /case2-precheck: library status passed/);
|
||||
});
|
||||
|
||||
test("--library-status takes no --profile", (t) => {
|
||||
const x = world(t);
|
||||
const r = precheck(x, { args: ["--business", "acme", "--library-status", "--profile", "library"] });
|
||||
assert.equal(r.code, 4, r.out);
|
||||
});
|
||||
|
||||
test("Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent", (t) => {
|
||||
const x = world(t);
|
||||
const file = join(x.dataRoot, "notify", "acme", "notify.json");
|
||||
const before = readFileSync(file), beforeMode = mode(file);
|
||||
const r = install(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.deepEqual(readFileSync(file), before);
|
||||
assert.equal(mode(file), beforeMode);
|
||||
const absent = world(t), missing = join(absent.dataRoot, "notify", "acme", "notify.json");
|
||||
rmSync(missing);
|
||||
const fresh = install(absent);
|
||||
assert.equal(fresh.code, 0, fresh.out);
|
||||
assert.equal(existsSync(missing), false);
|
||||
});
|
||||
@@ -626,9 +626,6 @@ function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\
|
||||
script(join(bin, "id"), `if [ "$1" = -G ]; then echo "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
|
||||
script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
|
||||
script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
|
||||
const notify = join(home, ".mosaic-dev", "notify", "acme");
|
||||
mkdirSync(notify, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(notify, "notify.json"), '{"notifyVersion":1,"binding":null}\n', { mode: 0o600 });
|
||||
return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
|
||||
}
|
||||
|
||||
@@ -875,68 +872,3 @@ test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind
|
||||
assert.equal(bindIn(LOW_PORT), "EACCES");
|
||||
assert.equal(bindIn(high), "bound");
|
||||
});
|
||||
|
||||
test("Vikunja 614 factory: scoped @anthropic-ai Claude path renders and passes installed read-back", (t) => {
|
||||
const h = hub(t);
|
||||
const claude = script(join(h.root, "opt", "claude-pin", "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"), "exit 0");
|
||||
// The suite runs unprivileged. Model root ownership for this regular executable
|
||||
// and its ancestors only; file type/executable checks and both unit readers are real.
|
||||
const ancestors = [];
|
||||
for (let p = claude; p !== "/"; p = dirname(p)) ancestors.push(p);
|
||||
script(join(h.bin, "stat"), `if [ "$1" = -c ] && [ "$3" = -- ]; then
|
||||
case "$4" in ${ancestors.map((p) => JSON.stringify(p)).join("|")})
|
||||
case "$2" in %u) echo 0; exit ;; %a) echo 755; exit ;; esac ;;
|
||||
esac
|
||||
fi
|
||||
exec /usr/bin/stat "$@"`);
|
||||
const first = unit(h, { profile: "factory", extra: ["--claude", claude] });
|
||||
assert.equal(first.code, 0, first.out + first.err);
|
||||
assert.ok(lines(read(busFile(h)), "Environment").includes(`MOSAIC_CLAUDE_BIN=${claude}`));
|
||||
const again = unit(h, { profile: "factory", copy: COPY_B, extra: ["--claude", claude] });
|
||||
assert.equal(again.code, 0, again.out + again.err);
|
||||
assert.ok(lines(read(busFile(h)), "Environment").includes(`MOSAIC_CLAUDE_BIN=${claude}`));
|
||||
assert.deepEqual(lines(read(busFile(h)), "WorkingDirectory"), [copyDir(h, COPY_B)]);
|
||||
});
|
||||
|
||||
test("Vikunja 614 factory: plain paths still refuse every named unsafe character", (t) => {
|
||||
const h = hub(t);
|
||||
for (const char of [" ", "%", "$", "\"", "'", "\\", "&", "|"]) {
|
||||
refuses(unit(h, { profile: "factory", extra: ["--claude", `/opt/claude${char}bin`] }), /--claude contains a character the unit cannot carry/);
|
||||
}
|
||||
});
|
||||
|
||||
test("Vikunja 614 precheck: explicit null and named notifier decisions pass", (t) => {
|
||||
for (const binding of [null, "host-notify"]) {
|
||||
const x = host(t);
|
||||
const dir = join(x.dataRoot, "notify", "acme");
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(dir, "notify.json"), JSON.stringify({ notifyVersion: 1, binding }), { mode: 0o600 });
|
||||
const r = precheck(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.ok(r.out.includes(`ok notifier config ${dir}/notify.json validates (directory 0700, file 0600)`), r.out);
|
||||
}
|
||||
});
|
||||
|
||||
test("Vikunja 614 precheck: notifier config refuses missing, public, malformed and extra-key decisions", (t) => {
|
||||
for (const fault of ["missing", "mode", "directory", "extra", "json", "binding", "version", "symlink"]) {
|
||||
const x = host(t);
|
||||
const dir = join(x.dataRoot, "notify", "acme"), file = join(dir, "notify.json");
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(file, '{"notifyVersion":1,"binding":null}\n', { mode: 0o600 });
|
||||
if (fault === "missing") rmSync(file);
|
||||
if (fault === "mode") chmodSync(file, 0o644);
|
||||
if (fault === "directory") chmodSync(dir, 0o755);
|
||||
if (fault === "extra") writeFileSync(file, '{"notifyVersion":1,"binding":null,"extra":true}\n');
|
||||
if (fault === "json") writeFileSync(file, '{bad json\n');
|
||||
if (fault === "binding") writeFileSync(file, '{"notifyVersion":1,"binding":"bad binding"}\n');
|
||||
if (fault === "version") writeFileSync(file, '{"notifyVersion":2,"binding":null}\n');
|
||||
if (fault === "symlink") {
|
||||
const target = join(dir, "target.json");
|
||||
writeFileSync(target, readFileSync(file), { mode: 0o600 });
|
||||
rmSync(file); symlinkSync(target, file);
|
||||
}
|
||||
const r = precheck(x);
|
||||
assert.equal(r.code, 1, `${fault}: ${r.out}`);
|
||||
assert.match(r.out, /REFUSED notifier config:/, `${fault}: ${r.out}`);
|
||||
}
|
||||
});
|
||||
@@ -316,28 +316,6 @@ test("row 194 CLI V5: delete polls to the transaction and the read lists the nam
|
||||
assert.match(failed.err, new RegExp(`^mosaic: workspace-rename-failed: .*run the same delete again to retry the rename \\(request [^,]+, transaction ${s.log().at(-1).transaction}\\)$`, "m"));
|
||||
});
|
||||
|
||||
// Row 239 (#1745): a fire's or a delete's revoke runs under the writer's lock. A delete closed after
|
||||
// that revoke ran says the tokens may already be revoked; a plain revision-conflict keeps its text.
|
||||
test("row 239 T5 CLI: a delete closed -revoked says the tokens may already be revoked; a plain revision-conflict doesn't", async (t) => {
|
||||
let refusal = "revision-conflict-revoked";
|
||||
const s = await setup(t, { hook: closing(() => ({ refusal })) });
|
||||
const added = await s.mosaic(["instance", "add", "acme", "sec-1", "--definition", "security-reviewer", "--revision", await s.revision()]);
|
||||
assert.equal(added.code, 0, added.err);
|
||||
const del = async () => s.mosaic(["instance", "delete", "acme", "sec-1", "--revision", await s.revision()]);
|
||||
let r = await del();
|
||||
assert.equal(r.code, 3, r.err);
|
||||
assert.match(r.err, /^mosaic: revision-conflict-revoked: the business file changed after the agent's tokens were revoked .*; the tokens may already be revoked .*which is safe \(request [^)]+\)$/m);
|
||||
refusal = "outcome-unknown-revoked";
|
||||
r = await del();
|
||||
assert.equal(r.code, 3, r.err);
|
||||
assert.match(r.err, /^mosaic: outcome-unknown-revoked: the agent's tokens may already be revoked and the write's outcome is unknown; .* \(request [^)]+\)$/m);
|
||||
refusal = "revision-conflict";
|
||||
r = await del();
|
||||
assert.equal(r.code, 3, r.err);
|
||||
assert.match(r.err, /^mosaic: revision-conflict: the business file changed since that revision; .* \(request [^)]+\)$/m);
|
||||
assert.doesNotMatch(r.err, /may already be revoked/);
|
||||
});
|
||||
|
||||
test("row 194 CLI: lifecycle and veto usage errors exit 4 before any bus call", async (t) => {
|
||||
const s = await setup(t);
|
||||
const rev = "a".repeat(64);
|
||||
|
||||
@@ -347,25 +347,3 @@ test('row 232: check --units external reads the system units, the default reads
|
||||
rmSync(argv);
|
||||
}
|
||||
});
|
||||
|
||||
test('Vikunja 614: bundle and checkout copy leave an unreadable caller directory before find',t=>{
|
||||
const s=setup(t),cwd=join(s.root,'unreadable');mkdirSync(cwd);
|
||||
const bundle=join(s.root,'release.bundle');git(s.repo,'bundle','create','-q',bundle,'refactor');
|
||||
// Enter while searchable, then reproduce a cwd inherited across a uid drop:
|
||||
// the caller can neither read nor search it. Absolute bundle lookup must work.
|
||||
const enter='cd "$1" && chmod 000 . && [ ! -r . ] && [ ! -x . ] && shift && exec "$@"';
|
||||
for(const [name,args] of [['bundle',['copy','--bundle',bundle,s.id]],['checkout',['copy',s.id]]]){
|
||||
const copies=join(s.root,`copies-${name}`);
|
||||
try{
|
||||
const r=spawnSync('/bin/sh',['-c',enter,'stack-copy-cwd',cwd,join(s.repo,'scripts','stack-copy.sh'),...args,'--copies',copies],{env:s.env,encoding:'utf8',timeout:30000});
|
||||
assert.equal(statSync(cwd).mode&0o555,0,'the inherited cwd is neither readable nor searchable');
|
||||
assert.equal(r.status,0,r.stdout+r.stderr);
|
||||
assert.doesNotMatch(r.stderr,/Failed to (restore initial working|change) directory|Permission denied/);
|
||||
assert.ok(existsSync(join(copies,s.id,'scripts','mosaic')));
|
||||
assert.equal(statSync(join(copies,s.id)).mode&0o777,0o555);
|
||||
}finally{
|
||||
// Restore even after a failed assertion/mutant, before setup's recursive cleanup.
|
||||
chmodSync(cwd,0o700);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -45,8 +45,6 @@ test("a bus code on stderr becomes the exit code; garbage and timeouts are outco
|
||||
test("busExit and refuseInsideAgent", () => {
|
||||
assert.equal(busExit("unauthenticated"), 3);
|
||||
assert.equal(busExit("invalid-request"), 2);
|
||||
// Row 239 (#1745): a write whose outcome is unknown after its revoke exits as outcome-unknown does.
|
||||
assert.equal(busExit("outcome-unknown-revoked"), 1);
|
||||
assert.doesNotThrow(() => refuseInsideAgent(humanEnv()));
|
||||
assert.throws(() => refuseInsideAgent({ PI_AGENT_DIR: "/x" }), (e) => e.exitCode === 3 && /PI_AGENT_DIR/.test(e.message));
|
||||
});
|
||||
@@ -48,9 +48,6 @@ import { hostname } from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { scan, markSeen, seenKey, ConfigError, loadRegistrations } from "./scan.mjs";
|
||||
import { createReader, rootsFromSpecs } from "../../conversation/src/reader.mjs";
|
||||
import { Refusal } from "../../conversation/src/safe-fs.mjs";
|
||||
import { loadSeatSets } from "../../business/src/library-seats.mjs";
|
||||
import { configDir } from "../../business/src/business.mjs";
|
||||
|
||||
const MAX_BODY = 4096;
|
||||
|
||||
@@ -173,8 +170,6 @@ export function foreignRequest(req) {
|
||||
export const REFUSAL_STATUS = {
|
||||
"unknown-conversation": 404,
|
||||
"unknown-branch": 404,
|
||||
"conversation-not-listed": 404,
|
||||
"conversation-business-unreadable": 503,
|
||||
unavailable: 404,
|
||||
"cursor-unknown": 409,
|
||||
"cursor-expired": 409,
|
||||
@@ -215,8 +210,7 @@ function conversationQuery(url) {
|
||||
export function conversationResponse(reader, url) {
|
||||
if (url.pathname === "/api/conversations") {
|
||||
if ([...url.searchParams.keys()].length) return { status: 400, body: { error: "no parameters are accepted" } };
|
||||
const listing = reader.catalogue();
|
||||
return listing.ok ? { status: 200, body: listing } : refusalResponse(listing);
|
||||
return { status: 200, body: reader.catalogue() };
|
||||
}
|
||||
const query = conversationQuery(url);
|
||||
if (query.error) return { status: 400, body: { error: query.error } };
|
||||
@@ -224,43 +218,17 @@ export function conversationResponse(reader, url) {
|
||||
? reader.next({ cursor: query.cursor, conversation: query.id, branch: query.branch })
|
||||
: reader.open({ conversation: query.id, branch: query.branch ?? null });
|
||||
if (out.ok) return { status: 200, body: out };
|
||||
return refusalResponse(out);
|
||||
}
|
||||
|
||||
function refusalResponse(out) {
|
||||
return { status: REFUSAL_STATUS[out.refusal.code] ?? 422, body: { error: out.refusal.message, refusal: { code: out.refusal.code, reconcile: out.refusal.reconcile } } };
|
||||
}
|
||||
|
||||
// Case 2 M7 (design 5.1): a root whose seat is creative in a library business is
|
||||
// hidden from the catalogue and from open and next. creativeCheck(dir) returns the
|
||||
// factory the reader calls once per public call: each call reads the business sets
|
||||
// once and applies that one answer to every root, so an edited business file takes
|
||||
// effect on the next call without a restart. A business file that can't be read
|
||||
// refuses the whole call with conversation-business-unreadable.
|
||||
export function creativeCheck(dir = configDir()) {
|
||||
return () => {
|
||||
let sets;
|
||||
try {
|
||||
sets = loadSeatSets({ dir });
|
||||
} catch (err) {
|
||||
throw new Refusal("conversation-business-unreadable", `a business file cannot be read: ${err.message}`);
|
||||
}
|
||||
return (root) => sets.creativeSeats.has(root.seat);
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPage(path = join(import.meta.dirname, "page.html")) {
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
// specs: agent specs to scan on each request. boardDir: where scan writes. configDirectory: the config
|
||||
// directory whose businesses/ the default conversation reader reads for M7 (the instance's, by default).
|
||||
export function createServer({ specs, boardDir, isAlive, now, seatsDir = null, discordDataRoot = null, page = loadPage(), isPidAlive, agentSend = DEFAULT_AGENT_SEND, exec = spawnSync, conversationReader = null, configDirectory = configDir() }) {
|
||||
// specs: agent specs to scan on each request. boardDir: where scan writes.
|
||||
export function createServer({ specs, boardDir, isAlive, now, seatsDir = null, discordDataRoot = null, page = loadPage(), isPidAlive, agentSend = DEFAULT_AGENT_SEND, exec = spawnSync, conversationReader = null }) {
|
||||
const rescan = () => scan(specs, { boardDir, isAlive, now, seatsDir, isPidAlive, discordDataRoot });
|
||||
const reader = conversationReader ?? createReader({
|
||||
roots: () => rootsFromSpecs(specs, loadRegistrations(seatsDir).registrations),
|
||||
creativeCheck: creativeCheck(configDirectory),
|
||||
});
|
||||
const reader = conversationReader ?? createReader({ roots: () => rootsFromSpecs(specs, loadRegistrations(seatsDir).registrations) });
|
||||
return createHttpServer((req, res) => {
|
||||
const refused = foreignRequest(req);
|
||||
if (refused) {
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// Case 2 M7 (design 5.1, row 220): a creative seat's conversations are hidden from
|
||||
// the catalogue, from open and from next. The thread store is a second wall: a
|
||||
// root built over it is hidden, and no spec under .pi/state reaches it. A business
|
||||
// file the board can't read refuses the listing and open, never a partial list.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { conversationResponse, creativeCheck, startServer, REFUSAL_STATUS } from "../src/serve.mjs";
|
||||
import { conversationId, createReader, rootsFromSpecs } from "../../conversation/src/reader.mjs";
|
||||
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
|
||||
import { mappingEntry } from "../../library/tests/helpers.mjs";
|
||||
|
||||
// A config directory holding one library business whose seat "bean" is creative,
|
||||
// and a thread store for bean's session under <data>/threads/<business>/<thread>.
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), "row220-board-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const dir = join(root, "config");
|
||||
const doc = businessDoc(root, "acme");
|
||||
doc.projects = { books: mappingEntry(), ...doc.projects };
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
writeJson(join(dir, "businesses", "acme.json"), doc);
|
||||
const threadDir = join(root, "data", "threads", "acme", "t1", "session");
|
||||
mkdirSync(threadDir, { recursive: true });
|
||||
writeFileSync(join(threadDir, "s.jsonl"), "{}\n");
|
||||
const threadRoot = { seat: "bean", project: "acme", projectRoot: join(root, "data"), dir: threadDir, harness: "pi", unsupportedReason: null, engineStartedAt: null };
|
||||
return { root, dir, threadDir, threadRoot };
|
||||
}
|
||||
|
||||
function breakBusinessFile(dir) {
|
||||
const file = join(dir, "businesses", "broken.json");
|
||||
writeFileSync(file, "{");
|
||||
chmodSync(file, 0o600);
|
||||
}
|
||||
|
||||
const detail = (id) => new URL(`/api/conversation?id=${encodeURIComponent(id)}`, "http://localhost");
|
||||
const listing = new URL("/api/conversations", "http://localhost");
|
||||
|
||||
test("M7: the check hides the creative seat, shows the others, and refuses when a business file is unreadable", (t) => {
|
||||
const f = fixture(t);
|
||||
const hidden = creativeCheck(f.dir)();
|
||||
assert.equal(hidden({ seat: "bean" }), true);
|
||||
assert.equal(hidden({ seat: "pm" }), false);
|
||||
breakBusinessFile(f.dir);
|
||||
assert.throws(() => creativeCheck(f.dir)(), (err) => err.code === "conversation-business-unreadable");
|
||||
});
|
||||
|
||||
test("M7: a thread store is not a root from a spec, and a root built over it is hidden from the list and from open", (t) => {
|
||||
const f = fixture(t);
|
||||
// The spec layout check in rootsFromSpecs drops a thread-store directory.
|
||||
assert.deepEqual(rootsFromSpecs([{ agent: "bean", project: "acme", sessionsDir: f.threadDir }]), []);
|
||||
const reader = createReader({ roots: [f.threadRoot], creativeCheck: creativeCheck(f.dir) });
|
||||
const cat = reader.catalogue();
|
||||
assert.deepEqual([cat.conversations, cat.refusedRoots], [[], []], "the root is hidden, not refused");
|
||||
const out = conversationResponse(reader, detail(conversationId(f.threadRoot, "s.jsonl")));
|
||||
assert.equal(out.status, REFUSAL_STATUS["conversation-not-listed"]);
|
||||
assert.equal(out.status, 404);
|
||||
assert.equal(out.body.refusal.code, "conversation-not-listed");
|
||||
});
|
||||
|
||||
test("M7: with an unreadable business file, a known and an unknown id both refuse with 503", (t) => {
|
||||
const f = fixture(t);
|
||||
breakBusinessFile(f.dir);
|
||||
const reader = createReader({ roots: [f.threadRoot], creativeCheck: creativeCheck(f.dir) });
|
||||
const known = conversationResponse(reader, detail(conversationId(f.threadRoot, "s.jsonl")));
|
||||
const unknown = conversationResponse(reader, detail(conversationId(f.threadRoot, "missing.jsonl")));
|
||||
for (const out of [known, unknown]) {
|
||||
assert.equal(out.status, 503);
|
||||
assert.equal(out.body.refusal.code, "conversation-business-unreadable");
|
||||
}
|
||||
});
|
||||
|
||||
test("M7: an unreadable business file refuses the listing and open with 503, never a partial list", (t) => {
|
||||
const f = fixture(t);
|
||||
breakBusinessFile(f.dir);
|
||||
const reader = createReader({ roots: [f.threadRoot], creativeCheck: creativeCheck(f.dir) });
|
||||
const cat = conversationResponse(reader, listing);
|
||||
assert.equal(cat.status, 503);
|
||||
assert.equal(cat.body.refusal.code, "conversation-business-unreadable");
|
||||
assert.equal(cat.body.conversations, undefined);
|
||||
const opened = conversationResponse(reader, detail(conversationId(f.threadRoot, "s.jsonl")));
|
||||
assert.equal(opened.status, REFUSAL_STATUS["conversation-business-unreadable"]);
|
||||
assert.equal(opened.status, 503);
|
||||
});
|
||||
|
||||
// C2: every test above builds its own reader. This one starts the board with no
|
||||
// injected reader, so the check reaches the reader only through createServer's
|
||||
// wiring (serve.mjs: `creativeCheck: creativeCheck(configDirectory)`). Deleting that
|
||||
// line shows bean's session in the listing and fails the first assertion below.
|
||||
// It also shows the business file in the fixture's config directory is the one
|
||||
// read: bean is hidden and pm is not, and no other file says so.
|
||||
test("M7: the board's default reader hides a creative seat's session from the listing and the detail route", async (t) => {
|
||||
const f = fixture(t);
|
||||
const proj = join(f.root, "proj");
|
||||
const specs = [];
|
||||
for (const [n, seat] of ["bean", "pm"].entries()) {
|
||||
const sessionsDir = join(proj, ".pi", "state", seat, "sessions");
|
||||
mkdirSync(sessionsDir, { recursive: true });
|
||||
const header = { type: "session", version: 3, id: `c0ffee00-0000-4000-8000-00000000000${n + 1}`, timestamp: "2026-10-22T12:00:00.000Z", cwd: proj };
|
||||
const message = { type: "message", id: "e0", parentId: null, timestamp: "2026-10-22T12:00:01.000Z", message: { role: "user", content: [{ type: "text", text: `${seat.toUpperCase()}-TEXT` }] } };
|
||||
writeFileSync(join(sessionsDir, "s.jsonl"), [header, message].map((l) => JSON.stringify(l)).join("\n") + "\n");
|
||||
specs.push({ agent: seat, project: "proj", sessionsDir, tmux: {} });
|
||||
}
|
||||
const roots = rootsFromSpecs(specs);
|
||||
const idOf = (seat) => conversationId(roots.find((r) => r.seat === seat), "s.jsonl");
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir: join(f.root, "board"), seatsDir: join(f.root, "seats"), configDirectory: f.dir, isAlive: () => true, page: "<html></html>" });
|
||||
t.after(() => { server.closeAllConnections(); server.close(); });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const get = async (path) => {
|
||||
const res = await fetch(base + path);
|
||||
return { status: res.status, text: await res.text() };
|
||||
};
|
||||
const cat = await get("/api/conversations");
|
||||
assert.equal(cat.status, 200, cat.text);
|
||||
assert.deepEqual(JSON.parse(cat.text).conversations.map((c) => c.seat), ["pm"]);
|
||||
assert.ok(!cat.text.includes("BEAN-TEXT"));
|
||||
const hidden = await get(`/api/conversation?id=${encodeURIComponent(idOf("bean"))}`);
|
||||
assert.equal(hidden.status, 404, hidden.text);
|
||||
assert.equal(JSON.parse(hidden.text).refusal.code, "conversation-not-listed");
|
||||
assert.ok(!hidden.text.includes("BEAN-TEXT"));
|
||||
const shown = await get(`/api/conversation?id=${encodeURIComponent(idOf("pm"))}`);
|
||||
assert.equal(shown.status, 200, shown.text);
|
||||
// The file is read on every call: a broken business file now refuses the same route.
|
||||
breakBusinessFile(f.dir);
|
||||
const refused = await get("/api/conversations");
|
||||
assert.equal(refused.status, 503, refused.text);
|
||||
assert.equal(JSON.parse(refused.text).refusal.code, "conversation-business-unreadable");
|
||||
});
|
||||
@@ -120,7 +120,7 @@ test('server rescans connector discovery and refuses HTTP reply without transpor
|
||||
try {
|
||||
const sessionsDir = join(f.root, 'sessions', 'discord-demo'), seatsDir = join(f.root, 'seats');
|
||||
writeRegistration(seatsDir, makeRegistration({ resolved: { seat: 'forged', project: 'fleet', sessionsDir, seatDir: sessionsDir, launchScript: join(f.root, 'unused.sh'), layout: 'repo', defaultWorkspace: f.root }, task: 'forged', tmux: { session: 'forged', socket: null }, pid: process.pid }));
|
||||
server = await startServer({ port: 0, specs: [], seatsDir, configDirectory: f.root, boardDir: join(f.root, 'board'), discordDataRoot: f.root, exec: () => { throw Error('transport forbidden'); } });
|
||||
server = await startServer({ port: 0, specs: [], seatsDir, boardDir: join(f.root, 'board'), discordDataRoot: f.root, exec: () => { throw Error('transport forbidden'); } });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
let response = await fetch(base + '/api/board'); const board = await response.json();
|
||||
assert.equal(board.sessions[0].state, 'idle'); assert.equal(board.sessions[0].connector.alive, true);
|
||||
|
||||
@@ -23,7 +23,7 @@ import { request as httpRequest } from "node:http";
|
||||
import { ConfigError, markSeen } from "../src/scan.mjs";
|
||||
import { isLoopbackHost, startServer, DEFAULT_AGENT_SEND, REPLY_LIMIT, REPLY_TRAILER, REFUSAL_STATUS } from "../src/serve.mjs";
|
||||
import { writeRegistration, makeRegistration } from "../../seat/src/seat.mjs";
|
||||
import { UNSUPPORTED_HARNESS, NOT_LISTED } from "../../conversation/src/reader.mjs";
|
||||
import { UNSUPPORTED_HARNESS } from "../../conversation/src/reader.mjs";
|
||||
|
||||
const pkgRoot = resolve(import.meta.dirname, "..");
|
||||
const cli = join(pkgRoot, "src", "cli.mjs");
|
||||
@@ -725,7 +725,7 @@ function replyFixture({ tmux = { socket: null, session: "agent1" }, pid = proces
|
||||
}
|
||||
const capture = join(root, "capture.json");
|
||||
process.env.FAKE_SEND_CAPTURE = capture;
|
||||
return { root, sessionsDir, seatsDir, configDirectory: root, boardDir: join(root, "board"), agentSend: writeFakeAgentSend(root), capture,
|
||||
return { root, sessionsDir, seatsDir, boardDir: join(root, "board"), agentSend: writeFakeAgentSend(root), capture,
|
||||
specs: [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }] };
|
||||
}
|
||||
|
||||
@@ -736,7 +736,7 @@ async function postReply(base, body, headers = { "content-type": "application/js
|
||||
|
||||
test("POST /api/reply: runs agent-send.sh with -s from the registration, -S <host>:control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams", async () => {
|
||||
const f = replyFixture();
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend, now: () => new Date("2026-09-12T18:00:00Z") });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend, now: () => new Date("2026-09-12T18:00:00Z") });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
process.env.MOSAIC_TMUX_SOCKET = "leaked-from-launcher";
|
||||
delete process.env.FAKE_SEND_EXIT;
|
||||
@@ -768,7 +768,7 @@ test("POST /api/reply: runs agent-send.sh with -s from the registration, -S <hos
|
||||
|
||||
test("POST /api/reply: a registration with a tmux socket adds -L <socket>", async () => {
|
||||
const f = replyFixture({ tmux: { socket: "mosaic-fleet", session: "agent1" } });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
const { status, body } = await postReply(base, { agent: "proj/agent1", text: "hi" });
|
||||
@@ -783,7 +783,7 @@ test("POST /api/reply: a registration with a tmux socket adds -L <socket>", asyn
|
||||
|
||||
test("POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim", async () => {
|
||||
const f = replyFixture();
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
process.env.FAKE_SEND_EXIT = "4";
|
||||
process.env.FAKE_SEND_STDERR = "agent-send.sh: REFUSING - session 'agent1' exists on multiple sockets\n";
|
||||
@@ -814,7 +814,7 @@ test("POST /api/reply: refusals before the tool runs: empty or blank or long tex
|
||||
];
|
||||
for (const c of cases) {
|
||||
const f = c.fixture;
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend, isPidAlive: c.isPidAlive });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend, isPidAlive: c.isPidAlive });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
const { status, body } = await postReply(base, c.body);
|
||||
@@ -829,7 +829,7 @@ test("POST /api/reply: refusals before the tool runs: empty or blank or long tex
|
||||
|
||||
test("POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash", async () => {
|
||||
const f = replyFixture();
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: join(f.root, "missing-agent-send.sh") });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: join(f.root, "missing-agent-send.sh") });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
const { status, body } = await postReply(base, { agent: "proj/agent1", text: "hi" });
|
||||
@@ -964,7 +964,7 @@ function rawRequest(port, { method = "GET", path = "/api/board", headers = {}, b
|
||||
|
||||
test("Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers", async () => {
|
||||
const f = replyFixture();
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const port = server.address().port;
|
||||
const own = `127.0.0.1:${port}`;
|
||||
const reply = JSON.stringify({ agent: "proj/agent1", text: "rebound page" });
|
||||
@@ -1003,7 +1003,7 @@ test("Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Hos
|
||||
|
||||
test("Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin", async () => {
|
||||
const f = replyFixture();
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", agentSend: f.agentSend });
|
||||
const port = server.address().port;
|
||||
delete process.env.FAKE_SEND_EXIT;
|
||||
delete process.env.FAKE_SEND_STDERR;
|
||||
@@ -1049,8 +1049,7 @@ function conversationFixture() {
|
||||
resolved: { seat: "rocko", project: "proj", sessionsDir: join(proj, ".pi", "state", "rocko", "sessions"), seatDir: join(root, "rocko"), launchScript: join(root, "rocko", "launch.sh"), layout: "repo", defaultWorkspace: null },
|
||||
harness: "claude-code",
|
||||
}));
|
||||
// Row 220 M7: the default reader reads <root>/businesses, which does not exist here, never the real config.
|
||||
return { root, proj, sessionsDir, seatsDir, configDirectory: root, boardDir: join(root, "board"), specs: [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }] };
|
||||
return { root, proj, sessionsDir, seatsDir, boardDir: join(root, "board"), specs: [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }] };
|
||||
}
|
||||
|
||||
// Size, sha256, mtime, (dev, ino) and listing of every entry under dir.
|
||||
@@ -1070,13 +1069,8 @@ function createHashHex(buf) {
|
||||
test("conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers", async () => {
|
||||
const f = conversationFixture();
|
||||
const calls = [];
|
||||
const spy = {
|
||||
// The route reads .ok on the listing, so the catalogue spy returns a listing, not the call count.
|
||||
catalogue: () => { calls.push("catalogue"); return { ok: true, conversations: [], refusedRoots: [] }; },
|
||||
open: () => calls.push("open"),
|
||||
next: () => calls.push("next"),
|
||||
};
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", conversationReader: spy });
|
||||
const spy = { catalogue: () => calls.push("catalogue"), open: () => calls.push("open"), next: () => calls.push("next") };
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", conversationReader: spy });
|
||||
const port = server.address().port;
|
||||
const own = `127.0.0.1:${port}`;
|
||||
const cases = [
|
||||
@@ -1102,34 +1096,18 @@ test("conversation routes (F16): a foreign Host, a wrong port and a cross-origin
|
||||
}
|
||||
});
|
||||
|
||||
test("conversation routes: a reader listing that refuses gets its refusal status and body, not a 200", async () => {
|
||||
const f = conversationFixture();
|
||||
const refusal = { code: "conversation-business-unreadable", reconcile: "fix the business file", message: "a business file can't be read" };
|
||||
const spy = { catalogue: () => ({ ok: false, refusal }), open: () => ({ ok: false, refusal }), next: () => ({ ok: false, refusal }) };
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>", conversationReader: spy });
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${server.address().port}/api/conversations`);
|
||||
assert.equal(res.status, 503);
|
||||
assert.deepEqual(await res.json(), { error: refusal.message, refusal: { code: refusal.code, reconcile: refusal.reconcile } });
|
||||
} finally {
|
||||
await closeServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
test("every refusal code the reader can raise has an HTTP status", () => {
|
||||
// Every source file in the reader package, so a refusal added in a new file is seen too.
|
||||
const dir = join(pkgRoot, "..", "conversation", "src");
|
||||
const src = readdirSync(dir).filter((f) => f.endsWith(".mjs")).map((f) => readFileSync(join(dir, f), "utf8")).join("\n");
|
||||
const codes = new Set([...src.matchAll(/new Refusal\(\s*"([a-z-]+)"/g)].map((m) => m[1]));
|
||||
codes.add(UNSUPPORTED_HARNESS); // raised by value, as a root's unsupportedReason
|
||||
codes.add(NOT_LISTED); // raised by value (row 220 M7), so the literal scan misses it
|
||||
codes.add("conversation-business-unreadable"); // raised by the board's creative predicate in serve.mjs (row 220 M7)
|
||||
assert.ok(codes.size >= 15, [...codes].join(" "));
|
||||
assert.deepEqual([...codes].filter((c) => !(c in REFUSAL_STATUS)), []);
|
||||
assert.deepEqual(Object.keys(REFUSAL_STATUS).filter((c) => !codes.has(c)), [], "no stale entries");
|
||||
// The statuses themselves, so a changed value fails here even where no route test reaches it.
|
||||
assert.deepEqual(REFUSAL_STATUS, {
|
||||
"unknown-conversation": 404, "unknown-branch": 404, unavailable: 404, "conversation-not-listed": 404, "conversation-business-unreadable": 503,
|
||||
"unknown-conversation": 404, "unknown-branch": 404, unavailable: 404,
|
||||
"cursor-unknown": 409, "cursor-expired": 409, "cursor-foreign": 409, "source-replaced": 409, "incomplete-header": 409,
|
||||
"unsafe-path": 403, "foreign-project": 403, unreadable: 403, "unknown-actor": 403,
|
||||
"unsupported-harness": 422, "not-a-pi-session": 422, "too-large": 422, "unsupported-purpose": 422,
|
||||
@@ -1139,7 +1117,7 @@ test("every refusal code the reader can raise has an HTTP status", () => {
|
||||
test("conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written", async () => {
|
||||
const f = conversationFixture();
|
||||
const before = treePrint(f.sessionsDir);
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, configDirectory: f.configDirectory, isAlive: () => true, page: "<html></html>" });
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>" });
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const get = async (path) => {
|
||||
const res = await fetch(base + path);
|
||||
|
||||
@@ -22,8 +22,6 @@ import { samePath } from "../../seat/src/seat.mjs";
|
||||
export const ACTOR = "local-operator";
|
||||
export const HISTORY = "pi";
|
||||
export const UNSUPPORTED_HARNESS = "unsupported-harness";
|
||||
// Case 2 design 5.1 M7: a creative root's conversations are not listed and do not open.
|
||||
export const NOT_LISTED = "conversation-not-listed";
|
||||
const NO_STREAM = "no-stream";
|
||||
const HEAD_BYTES = 1024 * 1024;
|
||||
const TAIL_BYTES = 2 * 1024 * 1024;
|
||||
@@ -203,20 +201,8 @@ function readSnapshot(root, name, pinnedLength = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// creativeCheck() is the caller's per-call answer: it returns a predicate that is
|
||||
// true when a root's seat is a creative instance on a business with a library
|
||||
// project (design 5.1 M7). The reader calls it once per public call (catalogue,
|
||||
// open, next), so one call reads the business sets once for every root. It may
|
||||
// throw (an unreadable business file), which refuses the whole call. The reader
|
||||
// only hides; it reads no business file itself. A truthy answer hides, so a
|
||||
// doubtful one fails closed. The answer must depend on root.seat and root.project
|
||||
// alone, because a cursor is checked against a root object from an earlier listing.
|
||||
export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 * 1000, maxCursors = 1000, cacheSize = 2, creativeCheck = () => () => false } = {}) {
|
||||
export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 * 1000, maxCursors = 1000, cacheSize = 2 } = {}) {
|
||||
const listRoots = typeof roots === "function" ? roots : () => roots ?? [];
|
||||
const hiddenCheck = () => {
|
||||
const answer = creativeCheck();
|
||||
return (root) => Boolean(answer(root));
|
||||
};
|
||||
const cursors = new Map();
|
||||
const parsedCache = new Map();
|
||||
const partsCache = new Map();
|
||||
@@ -255,34 +241,26 @@ export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 *
|
||||
return null;
|
||||
}
|
||||
|
||||
// A refusal from the hidden check (an unreadable business file) refuses the
|
||||
// whole listing; it never lists some roots and drops the rest silently.
|
||||
function catalogue() {
|
||||
try {
|
||||
const hidden = hiddenCheck();
|
||||
const conversations = [], refusedRoots = [];
|
||||
for (const root of listRoots()) {
|
||||
if (hidden(root)) continue;
|
||||
if (root.unsupportedReason) {
|
||||
conversations.push({ ...baseRow(root, unsupportedId(root)), engineStartedAt: root.engineStartedAt, availability: "unsupported" });
|
||||
continue;
|
||||
}
|
||||
let listing;
|
||||
try {
|
||||
listing = listSessionFiles(root);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Refusal)) throw err;
|
||||
refusedRoots.push({ seat: root.seat, project: root.project, refusal: err.code });
|
||||
continue;
|
||||
}
|
||||
for (const name of listing.files) conversations.push(summarize(root, name));
|
||||
for (const r of listing.refused) conversations.push({ ...baseRow(root, conversationId(root, r.name)), availability: "denied", refusal: r.code });
|
||||
const conversations = [], refusedRoots = [];
|
||||
for (const root of listRoots()) {
|
||||
if (root.unsupportedReason) {
|
||||
conversations.push({ ...baseRow(root, unsupportedId(root)), engineStartedAt: root.engineStartedAt, availability: "unsupported" });
|
||||
continue;
|
||||
}
|
||||
conversations.sort((a, b) => (b.lastActivityAt ?? "").localeCompare(a.lastActivityAt ?? "") || a.conversation.localeCompare(b.conversation));
|
||||
return { ok: true, conversations, refusedRoots, generatedAt: new Date(now()).toISOString() };
|
||||
} catch (err) {
|
||||
return refuse(err);
|
||||
let listing;
|
||||
try {
|
||||
listing = listSessionFiles(root);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Refusal)) throw err;
|
||||
refusedRoots.push({ seat: root.seat, project: root.project, refusal: err.code });
|
||||
continue;
|
||||
}
|
||||
for (const name of listing.files) conversations.push(summarize(root, name));
|
||||
for (const r of listing.refused) conversations.push({ ...baseRow(root, conversationId(root, r.name)), availability: "denied", refusal: r.code });
|
||||
}
|
||||
conversations.sort((a, b) => (b.lastActivityAt ?? "").localeCompare(a.lastActivityAt ?? "") || a.conversation.localeCompare(b.conversation));
|
||||
return { ok: true, conversations, refusedRoots, generatedAt: new Date(now()).toISOString() };
|
||||
}
|
||||
|
||||
// Parsed snapshot plus one branch's entries, cached by content. A null
|
||||
@@ -369,10 +347,8 @@ export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 *
|
||||
function open({ conversation, branch = null, actor = ACTOR, purpose = "history" } = {}) {
|
||||
try {
|
||||
checkCaller(actor, purpose);
|
||||
const hidden = hiddenCheck();
|
||||
const found = resolveConversation(conversation);
|
||||
if (!found) throw new Refusal("unknown-conversation", "no such conversation in the approved roots", { reconcile: true });
|
||||
if (hidden(found.root)) throw new Refusal(NOT_LISTED, "this conversation is not listed");
|
||||
if (found.root.unsupportedReason) throw new Refusal(found.root.unsupportedReason, "this harness has no history reader yet");
|
||||
const snap = snapshot(found.root, found.name);
|
||||
const built = build(snap, found.root, branch ?? null, conversation);
|
||||
@@ -386,7 +362,6 @@ export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 *
|
||||
|
||||
function next({ cursor, conversation, branch, actor = ACTOR, purpose = "history" } = {}) {
|
||||
try {
|
||||
const hidden = hiddenCheck();
|
||||
const held = typeof cursor === "string" ? cursors.get(cursor) : undefined;
|
||||
if (!held) throw new Refusal("cursor-unknown", "the cursor is unknown", { reconcile: true });
|
||||
const { record, state } = held;
|
||||
@@ -397,8 +372,6 @@ export function createReader({ roots, now = () => Date.now(), ttlMs = 10 * 60 *
|
||||
if (actor !== record.actor || purpose !== record.purpose || conversation !== record.conversation || branch !== record.branch) {
|
||||
throw new Refusal("cursor-foreign", "the cursor belongs to another view", { reconcile: true });
|
||||
}
|
||||
// A cursor issued before the root became creative stops here (M7).
|
||||
if (hidden(state.root)) throw new Refusal(NOT_LISTED, "this conversation is not listed");
|
||||
// The source must still be the snapshot's file with the same prefix.
|
||||
const pinned = snapshot(state.root, state.name, state.length);
|
||||
if (pinned.shorter) throw new Refusal("source-replaced", "the session file is shorter than the snapshot", { reconcile: true });
|
||||
|
||||
@@ -692,79 +692,6 @@ test("F15: a Claude seat is an unsupported-harness placeholder whose directory i
|
||||
}
|
||||
});
|
||||
|
||||
// M7 (Case 2 design 5.1): a creative seat's conversations are left out of the catalogue
|
||||
// and refuse to open by id. The fixture text stands in for creative prose and must not
|
||||
// appear in any output.
|
||||
test("M7: a creative seat's session is not listed and won't open by id; an assistant seat still lists", () => {
|
||||
const creative = project("bean");
|
||||
const assistant = project("sage");
|
||||
writeSession(creative.dir, "s.jsonl", header(creative.proj), [user("c1", null, "CREATIVE-MARKER", 1)]);
|
||||
writeSession(assistant.dir, "s.jsonl", header(assistant.proj), [user("a1", null, "assistant text", 1)]);
|
||||
const roots = rootsFromSpecs([creative.spec, assistant.spec]);
|
||||
const reader = createReader({ roots, creativeCheck: () => (root) => root.seat === "bean" });
|
||||
const cat = noWrites(tmp, () => reader.catalogue());
|
||||
assert.deepEqual(cat.conversations.map((c) => c.seat), ["sage"]);
|
||||
const hiddenOpen = noWrites(tmp, () => reader.open({ conversation: conversationId(roots.find((r) => r.seat === "bean"), "s.jsonl") }));
|
||||
assert.deepEqual([hiddenOpen.ok, hiddenOpen.refusal.code, hiddenOpen.refusal.reconcile], [false, "conversation-not-listed", false]);
|
||||
const shown = noWrites(tmp, () => reader.open({ conversation: conversationId(roots.find((r) => r.seat === "sage"), "s.jsonl") }));
|
||||
assert.deepEqual([shown.ok, texts(shown.page)], [true, ["assistant text"]]);
|
||||
assert.equal(JSON.stringify([cat, hiddenOpen]).includes("CREATIVE-MARKER"), false);
|
||||
});
|
||||
|
||||
test("M7: one catalogue calls the creative check once, however many roots it lists", () => {
|
||||
const creative = project("bean");
|
||||
const assistant = project("sage");
|
||||
writeSession(creative.dir, "s.jsonl", header(creative.proj), [user("c1", null, "one", 1)]);
|
||||
writeSession(assistant.dir, "s.jsonl", header(assistant.proj), [user("a1", null, "two", 1)]);
|
||||
const roots = rootsFromSpecs([creative.spec, assistant.spec]);
|
||||
let calls = 0;
|
||||
const reader = createReader({ roots, creativeCheck: () => { calls += 1; return (root) => root.seat === "bean"; } });
|
||||
const cat = noWrites(tmp, () => reader.catalogue());
|
||||
assert.deepEqual(cat.conversations.map((c) => c.seat), ["sage"]);
|
||||
assert.equal(calls, 1);
|
||||
const shown = noWrites(tmp, () => reader.open({ conversation: conversationId(roots.find((r) => r.seat === "sage"), "s.jsonl") }));
|
||||
assert.equal(shown.ok, true);
|
||||
assert.equal(calls, 2, "open is one more public call, so one more check");
|
||||
noWrites(tmp, () => reader.next({ cursor: shown.follow.id, conversation: conversationId(roots.find((r) => r.seat === "sage"), "s.jsonl"), branch: shown.page.branch }));
|
||||
assert.equal(calls, 3, "next is one more public call, so one more check");
|
||||
assert.deepEqual(cat.refusedRoots, [], "a hidden root is not a refused root");
|
||||
});
|
||||
|
||||
test("M7: a creative session already open refuses its next page once the root is hidden", () => {
|
||||
const fx = project("bean");
|
||||
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("c1", null, "CREATIVE-MARKER", 1), assistant("c2", "c1", "reply", 2)]);
|
||||
let hide = false;
|
||||
const roots = rootsFromSpecs([fx.spec]);
|
||||
const reader = createReader({ roots, creativeCheck: () => () => hide });
|
||||
const id = conversationId(roots[0], "s.jsonl");
|
||||
const first = noWrites(fx.base, () => reader.open({ conversation: id }));
|
||||
assert.equal(first.ok, true);
|
||||
hide = true;
|
||||
const r = noWrites(fx.base, () => reader.next({ cursor: first.follow.id, conversation: id, branch: first.page.branch }));
|
||||
assert.deepEqual([r.ok, r.refusal.code], [false, "conversation-not-listed"]);
|
||||
});
|
||||
|
||||
test("M7: a creative root's refused files and its unsupported placeholder are hidden as well", () => {
|
||||
const fx = project("bean");
|
||||
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("c1", null, "CREATIVE-MARKER", 1)]);
|
||||
symlinkSync(join(fx.dir, "s.jsonl"), join(fx.dir, "link.jsonl"));
|
||||
const chetDir = join(fx.proj, ".pi", "state", "chet", "sessions");
|
||||
const regs = [{ version: 1, seat: "chet", project: "proj", layout: "repo", harness: "claude-code", sessionsDir: chetDir, startedAt: time(-1) }];
|
||||
const roots = rootsFromSpecs([fx.spec], regs);
|
||||
const plain = noWrites(fx.base, () => createReader({ roots }).catalogue());
|
||||
const placeholder = plain.conversations.find((c) => c.availability === "unsupported");
|
||||
assert.ok(placeholder, "without M7 the unsupported placeholder is listed");
|
||||
const reader = createReader({ roots, creativeCheck: () => (root) => root.seat === "bean" || root.seat === "chet" });
|
||||
const cat = noWrites(fx.base, () => reader.catalogue());
|
||||
assert.deepEqual(cat.conversations, []);
|
||||
const bean = roots.find((r) => r.seat === "bean");
|
||||
for (const conversation of [placeholder.conversation, conversationId(bean, "link.jsonl"), conversationId(bean, "s.jsonl")]) {
|
||||
const r = noWrites(fx.base, () => reader.open({ conversation }));
|
||||
assert.deepEqual([r.ok, r.refusal.code], [false, "conversation-not-listed"]);
|
||||
}
|
||||
assert.equal(JSON.stringify(cat).includes("CREATIVE-MARKER"), false);
|
||||
});
|
||||
|
||||
test("unknown conversations, empty files and non-Pi files refuse", () => {
|
||||
const fx = project();
|
||||
writeFileSync(join(fx.dir, "empty.jsonl"), "");
|
||||
|
||||
@@ -47,12 +47,11 @@
|
||||
// not retry); 4 usage.
|
||||
|
||||
import { existsSync, mkdirSync, mkdtempSync, writeFileSync, statSync, readdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { DiscordError } from "./errors.mjs";
|
||||
import { assertProcessing, processingStatus, ProcessingError } from "../../processing-control/src/state.mjs";
|
||||
import { registerRuntime, updateRuntime, unregisterRuntime, processIdentity } from "../../processing-control/src/runtimes.mjs";
|
||||
import { loadSeatSets } from "../../business/src/library-seats.mjs";
|
||||
import { defaultConfigPath, loadDataRoot, bindingPath, bindingDataDir, loadBinding, readToken, resolveContextFiles, resolveToolRoots, reloadDiff } from "./binding.mjs";
|
||||
import { createRest } from "./rest.mjs";
|
||||
import { createGateway, CONNECTOR_INTENTS } from "./gateway.mjs";
|
||||
@@ -105,33 +104,12 @@ function warn(msg) {
|
||||
process.stderr.write(`discord: ${msg}\n`);
|
||||
}
|
||||
|
||||
// Case 2 M8 (design 5.1, row 220): a seat that works in a library business runs
|
||||
// only on the mosaic-bus engine, with no tools key. The business files come from
|
||||
// the config directory --config names. An unreadable file refuses; it never reads
|
||||
// as "no library seats".
|
||||
function checkLibraryEngine(configDirectory, binding) {
|
||||
let sets;
|
||||
try {
|
||||
sets = loadSeatSets({ dir: configDirectory });
|
||||
} catch (err) {
|
||||
throw new DiscordError(`discord-business-unreadable: ${err.message}`, 2);
|
||||
}
|
||||
if (!sets.librarySeats.has(binding.seat)) return;
|
||||
if (binding.engine.provider !== "mosaic-bus") {
|
||||
throw new DiscordError(`discord-engine-not-allowed: binding ${binding.name} runs seat ${binding.seat}, which works in a library business; its engine must be mosaic-bus, not ${binding.engine.provider}`, 2);
|
||||
}
|
||||
if (binding.tools !== null) {
|
||||
throw new DiscordError(`discord-engine-not-allowed: binding ${binding.name} runs seat ${binding.seat}, which works in a library business; a mosaic-bus binding there may not declare the tools key`, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Everything that can be checked without the network, shared by check and run.
|
||||
function prepare(opts) {
|
||||
const dataRoot = loadDataRoot(opts.config);
|
||||
const bindingFile = bindingPath(dataRoot, opts.binding);
|
||||
const binding = loadBinding(bindingFile);
|
||||
if (binding.name !== opts.binding) throw new DiscordError(`binding name ${JSON.stringify(binding.name)} does not match file name ${opts.binding}`);
|
||||
checkLibraryEngine(dirname(opts.config), binding);
|
||||
const contextFiles = resolveContextFiles(binding, opts.repo);
|
||||
const toolRoots = resolveToolRoots(binding, { dataRoot });
|
||||
const pi = join(opts.repo, "node_modules", ".bin", "pi");
|
||||
|
||||
@@ -77,9 +77,6 @@ export function makeDeployment(root, overrides = {}, { tokenMode = 0o600, tokenC
|
||||
writeFileSync(bindingFile, JSON.stringify(raw, null, 2), { mode: 0o600 });
|
||||
const config = join(root, "config.json");
|
||||
writeFileSync(config, JSON.stringify({ dataRoot }));
|
||||
// Row 220 M8: check and run read <root>/businesses beside --config. Nothing
|
||||
// there means no library seat, so a deployment starts with none; the tests
|
||||
// that need one write a business file into it.
|
||||
return { dataRoot, config, bindingFile, tokenFile, raw, journalDir: join(dataRoot, "discord", raw.name) };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// Case 2 M8 (design 5.1, row 220): a binding for a seat in a library business
|
||||
// runs only on the mosaic-bus engine with no tools key. The business files sit
|
||||
// in the directory beside --config. An unreadable file refuses with
|
||||
// discord-business-unreadable. Both verbs refuse: check runs with --offline, and
|
||||
// run refuses in prepare(), before the journal is made and before any network.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { makeRoot, makeRepo, makeDeployment } from "./helpers.mjs";
|
||||
import { initializeControl } from "../../processing-control/src/state.mjs";
|
||||
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
|
||||
import { mappingEntry } from "../../library/tests/helpers.mjs";
|
||||
|
||||
const CLI = fileURLToPath(new URL("../src/cli.mjs", import.meta.url));
|
||||
const MOSAIC_BUS = { provider: "mosaic-bus", model: "glm-5.3", thinking: "high" };
|
||||
|
||||
// overrides may be a function of the root, for fixtures that need a path under it.
|
||||
function setup(overrides = {}) {
|
||||
const root = makeRoot();
|
||||
const repo = makeRepo(root);
|
||||
const extra = typeof overrides === "function" ? overrides(root) : overrides;
|
||||
const d = makeDeployment(root, extra, { tokenContent: "MTAw.abcdefghijklmnopqrstuvwxyz0123456789" });
|
||||
return { root, repo, d };
|
||||
}
|
||||
|
||||
// Writes businesses/acme.json under the deployment's config directory. A library
|
||||
// business puts its roles (the seat "sage" here) in the library sets.
|
||||
function business(s, { library, role = "sage", definition = "companion" }) {
|
||||
const doc = businessDoc(s.root, "acme");
|
||||
if (library) doc.projects = { books: mappingEntry(), ...doc.projects };
|
||||
doc.roles[role] = { definition };
|
||||
writeJson(join(s.root, "businesses", "acme.json"), doc);
|
||||
}
|
||||
|
||||
// run stops at the processing gate first, so the deployment's control state is initialised.
|
||||
function run(s) {
|
||||
initializeControl(s.d.dataRoot);
|
||||
return spawnSync(process.execPath, [CLI, "run", s.d.raw.name, "--config", s.d.config, "--repo", s.repo], { encoding: "utf8", timeout: 10000 });
|
||||
}
|
||||
|
||||
function check(s) {
|
||||
return spawnSync(process.execPath, [CLI, "check", s.d.raw.name, "--config", s.d.config, "--repo", s.repo, "--offline"], { encoding: "utf8", timeout: 10000 });
|
||||
}
|
||||
|
||||
test("M8: a library seat's binding on a non-mosaic-bus engine is refused before any network", () => {
|
||||
const s = setup();
|
||||
business(s, { library: true });
|
||||
const r = check(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-engine-not-allowed: .*its engine must be mosaic-bus, not zai/);
|
||||
});
|
||||
|
||||
test("M8: a library seat's mosaic-bus binding that declares tools is refused, and the message names tools", () => {
|
||||
const s = setup((root) => ({ engine: MOSAIC_BUS, tools: { roots: [{ name: "docs", path: join(root, "docs") }] } }));
|
||||
business(s, { library: true });
|
||||
const r = check(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-engine-not-allowed: .*tools key/);
|
||||
});
|
||||
|
||||
test("M8: a library seat's mosaic-bus binding with no tools passes the engine rule", () => {
|
||||
const s = setup({ engine: MOSAIC_BUS });
|
||||
business(s, { library: true });
|
||||
const r = check(s);
|
||||
assert.doesNotMatch(r.stderr, /discord-engine-not-allowed|discord-business-unreadable/);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /offline check passed/);
|
||||
});
|
||||
|
||||
test("M8: an unreadable business file refuses check with discord-business-unreadable", () => {
|
||||
const s = setup();
|
||||
mkdirSync(join(s.root, "businesses"));
|
||||
writeFileSync(join(s.root, "businesses", "broken.json"), "{");
|
||||
chmodSync(join(s.root, "businesses", "broken.json"), 0o600);
|
||||
const r = check(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-business-unreadable/);
|
||||
});
|
||||
|
||||
test("M8: a binding on a seat that is in no library business checks clean, whatever its engine", () => {
|
||||
const s = setup();
|
||||
business(s, { library: false });
|
||||
const r = check(s);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /offline check passed/);
|
||||
});
|
||||
|
||||
test("M8: run refuses a library seat's raw engine with exit 2, before the journal is made and before any network", () => {
|
||||
const s = setup();
|
||||
business(s, { library: true });
|
||||
const r = run(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-engine-not-allowed: .*its engine must be mosaic-bus, not zai/);
|
||||
assert.equal(existsSync(s.d.journalDir), false, "nothing was started");
|
||||
});
|
||||
|
||||
test("M8: run refuses a library seat's mosaic-bus binding that declares tools", () => {
|
||||
const s = setup((root) => ({ engine: MOSAIC_BUS, tools: { roots: [{ name: "docs", path: join(root, "docs") }] } }));
|
||||
business(s, { library: true });
|
||||
const r = run(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-engine-not-allowed: .*tools key/);
|
||||
assert.equal(existsSync(s.d.journalDir), false);
|
||||
});
|
||||
|
||||
test("M8: an unreadable business file refuses run with discord-business-unreadable, before the journal is made", () => {
|
||||
const s = setup();
|
||||
mkdirSync(join(s.root, "businesses"));
|
||||
writeFileSync(join(s.root, "businesses", "broken.json"), "{");
|
||||
chmodSync(join(s.root, "businesses", "broken.json"), 0o600);
|
||||
const r = run(s);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /discord-business-unreadable/);
|
||||
assert.equal(existsSync(s.d.journalDir), false);
|
||||
});
|
||||
+23
-55
@@ -31,72 +31,33 @@ deep-frozen copy. `where` names the entry in messages. `fail(code, message)` def
|
||||
- `user` is the bot account name from design step 3, required for sync. It is optional in the
|
||||
mapping and checked with row 199's account name pattern, `[A-Za-z0-9_.-]`, 1 to 100 characters.
|
||||
- `vaultPrefix` is optional, one folder name.
|
||||
- `editor` is optional, `{ "name", "email" }` plus an optional `"user"`: the ident her Console saves commit under
|
||||
(C2-01d). Each value is text that is non-empty after trim, with no control character
|
||||
(`U+0000`-`U+001F`, `U+007F`-`U+009F`), `<` or `>`, and no whitespace or git-stripped character at
|
||||
either end. `user` is her Gitea login for the `library-human` slot (sage-2925), checked as the
|
||||
entry's `user` is. `editor` and its `user` come back only when given; a save without them refuses
|
||||
in C2-02.
|
||||
- `types` maps a folder name to `character`, `place`, `research`, `tracker` or `summary`.
|
||||
`manuscript` lists folder names and shares none with `types`.
|
||||
- A universe has `name`, `books` and `playground`. A hybrid adds `kind: "hybrid"`, two or more
|
||||
plain-universe `sources`, `constraints` (each one `{ "exclude": ... }` or
|
||||
`{ "pin": { "entity", "field", "value" } }`) and `notes`, and may have `books: []`. A draft
|
||||
universe has only `kind: "draft"`, `name` and one or more `playground` prefixes, and comes back as
|
||||
`{ kind: "draft", name, books: [], playground, supporting }`. `kind` is absent, `"hybrid"` or `"draft"`.
|
||||
`{ kind: "draft", name, books: [], playground }`. `kind` is absent, `"hybrid"` or `"draft"`.
|
||||
- A playground prefix ends in `/` and has one of three levels (C2-01c). Book level lies inside one of
|
||||
its own entry's book folders. Universe level, in a plain universe or a hybrid, lies outside every
|
||||
book folder. A draft's prefixes all lie outside every book folder. No prefix holds another or is
|
||||
listed twice, across every entry.
|
||||
- Every universe kind may also have `supporting`, a list of prefixes where agents may write (C2-01d,
|
||||
M12). It comes back on every universe, `[]` when absent. A supporting prefix follows the playground's
|
||||
rules: it ends in `/`, has the same three levels, holds no other supporting prefix and is listed
|
||||
once, across every entry.
|
||||
- Between the two lists: a supporting prefix may equal a playground prefix of its own entry, and a
|
||||
playground prefix may hold one of its own entry's supporting prefixes. A supporting prefix may not
|
||||
hold a playground prefix, which scope would then never reach, and may not equal, hold or lie inside
|
||||
another entry's playground prefix.
|
||||
|
||||
Refusals:
|
||||
|
||||
| Code | When |
|
||||
|---|---|
|
||||
| `library-playground-other-universe` | a playground prefix lies inside, or holds, a book folder of another entry |
|
||||
| `library-supporting-other-universe` | a supporting prefix lies inside, or holds, a book folder of another entry |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, an unknown `kind`, a book folder in two universes, a book folder that holds another, a playground or supporting prefix that holds another of its list or is listed twice, a prefix equal to a book folder or holding its own entry's book, a supporting prefix that holds a playground prefix or overlaps another entry's, a bad `editor`, a draft with `books` or with no prefix, a hybrid source that is unknown, a hybrid or a draft |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, an unknown `kind`, a book folder in two universes, a book folder that holds another, a playground prefix that holds another or is listed twice, a prefix equal to a book folder or holding its own entry's book, a draft with `books` or with no prefix, a hybrid source that is unknown, a hybrid or a draft |
|
||||
|
||||
## Scope
|
||||
|
||||
`classify(mapping, path)` decides scope from the path alone. Only paths ending in lowercase `.md` are
|
||||
mapped. It checks prefixes first, supporting before playground. A path under any entry's supporting
|
||||
prefix is `supporting`, and otherwise one under a playground prefix is `playground`. Either way that
|
||||
entry is `universe` and the book folder holding the prefix is `book`, or `null` at universe level and
|
||||
mapped. It checks prefixes first. A path under any entry's playground prefix is `playground`, with that
|
||||
entry as `universe` and the book folder holding the prefix as `book`, or `null` at universe level and
|
||||
in a draft. Any other path under a book folder is `canon`. Everything else is `unmapped`. The type is
|
||||
the first folder after the prefix, or after the book: a `types` name, `manuscript`, or `null`.
|
||||
|
||||
## Agent write paths
|
||||
|
||||
`supportingPath(mapping, universe, path, tree)` returns `path` when an agent may write it in that
|
||||
universe, else it refuses `library-write-story-path` (C2-01d, M12). `tree` is the array `listTree`
|
||||
returns for the commit the write lands on; anything else is a TypeError. It reads no git. The checks
|
||||
run in this order, so a path with two faults always names the same one:
|
||||
|
||||
1. a string of at most 1,024 UTF-8 bytes;
|
||||
2. no control character (the `editor` class), and the message leaves the path out;
|
||||
3. no backslash;
|
||||
4. not absolute;
|
||||
5. no empty, `.` or `..` segment, which also refuses `//` and a trailing `/`;
|
||||
6. `classify` gives `supporting` in that same universe, which refuses unmapped, `.canvas`, `.base`
|
||||
and attachment paths, canon, a playground-only folder and another universe's prefix;
|
||||
7. no folder on the way, and not the path itself, is a symlink (`120000`) or gitlink (`160000`) in
|
||||
`tree`;
|
||||
8. no folder on the way, and not the path itself, matches a tree path or tree folder after
|
||||
`normalize('NFC').toLowerCase()` unless it is that exact name. APFS folds case and Unicode form,
|
||||
so her checkout would put the file somewhere else. A tree that already holds case twins refuses a
|
||||
write to either (R7).
|
||||
|
||||
A delete's further rule, that the path exists at the tip as a `100644` blob, belongs to the C2-02
|
||||
broker.
|
||||
the first folder after the playground prefix, or after the book: a `types` name, `manuscript`, or
|
||||
`null`.
|
||||
|
||||
## Reader
|
||||
|
||||
@@ -143,21 +104,28 @@ 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).
|
||||
|
||||
## renderItems
|
||||
## renderReply
|
||||
|
||||
`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.
|
||||
`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:
|
||||
|
||||
```
|
||||
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)
|
||||
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"
|
||||
```
|
||||
|
||||
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.
|
||||
The broker's write notice, `{ "notice": "library-write", op, path, commit }`, gives the line
|
||||
`<name> wrote <path> @<commit>` or `<name> deleted <path> @<commit>`.
|
||||
|
||||
## Survey
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// The lookup index of design 3 (C2-05a): types, names, links and backlinks for one commit of her repo.
|
||||
// It is built from the reader's output, so it holds only mapped, readable files: R1, R2 and R7 leave a
|
||||
// file out, and an unresolved link is not in it. It is cached in memory by commit id. The key also
|
||||
// carries the repo path and a digest of the mapping, because one commit reads differently under another
|
||||
// mapping. The index is not universe-aware; query.mjs decides what a universe may see.
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readLibrary } from './reader.mjs';
|
||||
import { raise } from './problems.mjs';
|
||||
|
||||
const CACHE_LIMIT = 8;
|
||||
const cache = new Map();
|
||||
|
||||
const push = (map, key, value) => {
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key).push(value);
|
||||
};
|
||||
// A file's name for lookups: its front matter `name` when it is text, else its file name.
|
||||
const displayName = file => {
|
||||
const name = file.frontmatter?.name;
|
||||
return typeof name === 'string' && name.trim() ? name.trim() : file.name;
|
||||
};
|
||||
// A link as the index keeps it: the file at the other end, its line, and the field when front matter holds it.
|
||||
const linkAt = (path, link) => ({ path, line: link.line, ...(link.field === undefined ? {} : { field: link.field }) });
|
||||
|
||||
// Builds the index from readLibrary's output. Pure: no git, no clock.
|
||||
export function buildIndex(library, mapping) {
|
||||
const files = new Map(), types = new Map(), names = new Map(), linksOut = new Map(), linksIn = new Map();
|
||||
for (const file of library.files) {
|
||||
const name = displayName(file);
|
||||
files.set(file.path, {
|
||||
path: file.path, scope: file.scope, universe: file.universe, book: file.book, type: file.type,
|
||||
folder: file.folder, stem: file.name, name, frontmatter: file.frontmatter, lines: file.lines,
|
||||
});
|
||||
if (file.type !== null) push(types, file.type, file.path);
|
||||
push(names, name.toLowerCase(), file.path);
|
||||
linksOut.set(file.path, file.links.map(link => linkAt(link.path, link)));
|
||||
for (const link of file.links) push(linksIn, link.path, linkAt(file.path, link));
|
||||
}
|
||||
return Object.freeze({
|
||||
commit: library.commit, mapping, files, paths: [...files.keys()], types, names, linksOut, linksIn,
|
||||
problems: library.problems,
|
||||
});
|
||||
}
|
||||
|
||||
const digest = mapping => createHash('sha256').update(JSON.stringify(mapping)).digest('hex');
|
||||
|
||||
// The index for one commit under one mapping, read once and kept in memory. A repeat call returns the same
|
||||
// object. Past CACHE_LIMIT entries the oldest goes. A failed read is not kept.
|
||||
export async function indexFor({ repo, commit, mapping }) {
|
||||
if (!mapping || mapping.kind !== 'library') raise('library-mapping-invalid', 'expected a checked library mapping');
|
||||
const key = JSON.stringify([repo, commit, digest(mapping)]);
|
||||
if (cache.has(key)) {
|
||||
const hit = cache.get(key);
|
||||
cache.delete(key);
|
||||
cache.set(key, hit);
|
||||
return hit;
|
||||
}
|
||||
const pending = readLibrary({ repo, commit, mapping }).then(library => buildIndex(library, mapping));
|
||||
cache.set(key, pending);
|
||||
while (cache.size > CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
||||
try {
|
||||
return await pending;
|
||||
} catch (error) {
|
||||
if (cache.get(key) === pending) cache.delete(key);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearIndexCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function indexCacheSize() {
|
||||
return cache.size;
|
||||
}
|
||||
@@ -2,12 +2,9 @@
|
||||
export { LibraryError, RULES, problem, sortProblems } from './problems.mjs';
|
||||
export { checkMapping, TYPES } from './mapping.mjs';
|
||||
export { classify } from './scope.mjs';
|
||||
export { supportingPath } from './supporting.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 { renderItems, KINDS, NOTICES, UNREADABLE } from './render.mjs';
|
||||
export { renderReply, SCOPES, NOTICE_OPS } 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';
|
||||
export { buildIndex, indexFor, clearIndexCache, indexCacheSize } from './index-build.mjs';
|
||||
export { sees, find, get, search, manuscript, universeInfo, QUERY_REPLY_CAP, LOOKUP_LIMIT, MANUSCRIPT_LINES } from './query.mjs';
|
||||
@@ -1,9 +1,8 @@
|
||||
// The library project entry of a business file (design 1.1): her folders mapped onto universes,
|
||||
// books, type folders, manuscript folders and playground prefixes at three levels (ruling after r11,
|
||||
// C2-01c): inside one of the entry's books, outside every book, or in a draft universe. Supporting
|
||||
// prefixes (C2-01d) follow the same rules and are where agents may write (M12). A pure validator.
|
||||
// Business file loading wires it in C2-04, which passes its own `fail` so the refusal reads as a
|
||||
// business file problem. Agents can't write this entry (M9, C2-04).
|
||||
// C2-01c): inside one of the entry's books, outside every book, or in a draft universe. A pure validator. Business file
|
||||
// loading wires it in C2-04, which passes its own `fail` so the refusal reads as a business file
|
||||
// problem. Agents can't write this entry (M9, C2-04).
|
||||
import { raise } from './problems.mjs';
|
||||
|
||||
export const TYPES = Object.freeze(['character', 'place', 'research', 'tracker', 'summary']);
|
||||
@@ -13,12 +12,6 @@ const BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
||||
const USER = /^[A-Za-z0-9_.-]{1,100}$/;
|
||||
const CONSTRAINT_KEYS = ['exclude', 'pin'];
|
||||
const KINDS = ['hybrid', 'draft'];
|
||||
// C2-01d (F41): the editor ident her Console saves commit under. Git drops `<` and `>` from an ident
|
||||
// and commits under a different name, so they refuse here (webby-0169 R1).
|
||||
const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;
|
||||
// Git's ident.c crud() strips any byte <= 0x20 and , : ; < > " \ ' from both ends of an ident, so a
|
||||
// value that starts or ends with one would save one name and commit another (sage-3601, sage-3606).
|
||||
const IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/;
|
||||
|
||||
// A folder segment from her vault: spaces and capitals are fine, separators, control characters
|
||||
// and dot names are not.
|
||||
@@ -47,7 +40,7 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
};
|
||||
|
||||
object(entry, '');
|
||||
only(entry, ['kind', 'remote', 'branch', 'user', 'vaultPrefix', 'editor', 'universes', 'types', 'manuscript'], 'entry');
|
||||
only(entry, ['kind', 'remote', 'branch', 'user', 'vaultPrefix', 'universes', 'types', 'manuscript'], 'entry');
|
||||
if (entry.kind !== 'library') bad('kind must be "library"');
|
||||
let url;
|
||||
try { url = new URL(entry.remote); } catch { bad('remote must be an https:// URL'); }
|
||||
@@ -59,20 +52,6 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (Object.hasOwn(entry, 'user') && (typeof entry.user !== 'string' || !USER.test(entry.user)))
|
||||
bad('user must be a plain account name');
|
||||
if (Object.hasOwn(entry, 'vaultPrefix') && !segment(entry.vaultPrefix)) bad('vaultPrefix must be one folder name');
|
||||
if (Object.hasOwn(entry, 'editor')) {
|
||||
object(entry.editor, 'editor');
|
||||
only(entry.editor, ['name', 'email', 'user'], 'editor');
|
||||
// Her Gitea login for the library-human slot, which C2-02 passes in (sage-2925). Optional here.
|
||||
if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' || !USER.test(entry.editor.user)))
|
||||
bad('editor.user must be a plain account name');
|
||||
for (const key of ['name', 'email']) {
|
||||
const value = entry.editor[key];
|
||||
if (typeof value !== 'string' || !value.trim() || IDENT_BAD.test(value))
|
||||
bad(`editor.${key} must be non-empty text with no control character, < or >`);
|
||||
if (value.trim() !== value || IDENT_CRUD_END.test(value))
|
||||
bad(`editor.${key} must not start or end with whitespace or any of , : ; " ' \\`);
|
||||
}
|
||||
}
|
||||
|
||||
object(entry.types, 'types');
|
||||
for (const [name, type] of Object.entries(entry.types)) {
|
||||
@@ -85,7 +64,6 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
object(entry.universes, 'universes');
|
||||
if (!Object.keys(entry.universes).length) bad('universes must name at least one universe');
|
||||
const universes = {}, bookOwner = new Map(), prefixes = [], prefixOwner = new Map();
|
||||
const supports = [], supportOwner = new Map();
|
||||
for (const [id, universe] of Object.entries(entry.universes)) {
|
||||
const name = `universes.${id}`;
|
||||
if (!ID.test(id)) bad(`universe id ${JSON.stringify(id)} is not a plain id`);
|
||||
@@ -93,8 +71,8 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (Object.hasOwn(universe, 'kind') && !KINDS.includes(universe.kind)) bad(`${name}.kind must be "hybrid" or "draft"`);
|
||||
const hybrid = universe.kind === 'hybrid', draft = universe.kind === 'draft';
|
||||
if (draft && Object.hasOwn(universe, 'books')) bad(`${name} is a draft and takes no books`);
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'supporting', 'constraints', 'notes']
|
||||
: draft ? ['kind', 'name', 'playground', 'supporting'] : ['name', 'books', 'playground', 'supporting'], name);
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'constraints', 'notes']
|
||||
: draft ? ['kind', 'name', 'playground'] : ['name', 'books', 'playground'], name);
|
||||
if (typeof universe.name !== 'string' || !universe.name.trim() || universe.name.length > 128 || /[\u0000-\u001f]/.test(universe.name))
|
||||
bad(`${name}.name must be a short display name`);
|
||||
const books = draft ? [] : list(universe.books, `${name}.books`, folder, 'a folder path');
|
||||
@@ -103,14 +81,12 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (bookOwner.has(book)) bad(`book folder ${JSON.stringify(book)} is in two universes`);
|
||||
bookOwner.set(book, id);
|
||||
}
|
||||
const prefix = value => typeof value === 'string' && value.endsWith('/') && folder(value.slice(0, -1));
|
||||
const playground = list(universe.playground ?? [], `${name}.playground`, prefix, 'a folder path ending in /');
|
||||
const playground = list(universe.playground ?? [], `${name}.playground`,
|
||||
value => typeof value === 'string' && value.endsWith('/') && folder(value.slice(0, -1)), 'a folder path ending in /');
|
||||
if (draft && !playground.length) bad(`${name}.playground must name at least one prefix`);
|
||||
for (const p of playground) { prefixes.push(p); prefixOwner.set(p, id); }
|
||||
const supporting = list(universe.supporting ?? [], `${name}.supporting`, prefix, 'a folder path ending in /');
|
||||
for (const p of supporting) { supports.push(p); supportOwner.set(p, id); }
|
||||
if (draft) { universes[id] = { kind: 'draft', name: universe.name, books, playground, supporting }; continue; }
|
||||
const out = { name: universe.name, books, playground, supporting };
|
||||
for (const prefix of playground) { prefixes.push(prefix); prefixOwner.set(prefix, id); }
|
||||
if (draft) { universes[id] = { kind: 'draft', name: universe.name, books, playground }; continue; }
|
||||
const out = { name: universe.name, books, playground };
|
||||
if (hybrid) {
|
||||
out.kind = 'hybrid';
|
||||
out.sources = list(universe.sources, `${name}.sources`, value => typeof value === 'string' && ID.test(value), 'a universe id');
|
||||
@@ -148,36 +124,18 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
for (const a of books) for (const b of books) if (a !== b && holds(a, b)) bad(`book folder ${JSON.stringify(a)} holds book folder ${JSON.stringify(b)}`);
|
||||
for (const a of prefixes) for (const b of prefixes) if (a !== b && holds(a, b)) bad(`playground ${JSON.stringify(a)} holds playground ${JSON.stringify(b)}`);
|
||||
if (new Set(prefixes).size !== prefixes.length) bad('a playground prefix is listed twice');
|
||||
for (const a of supports) for (const b of supports) if (a !== b && holds(a, b)) bad(`supporting ${JSON.stringify(a)} holds supporting ${JSON.stringify(b)}`);
|
||||
if (new Set(supports).size !== supports.length) bad('a supporting prefix is listed twice');
|
||||
// A prefix's level comes from where it sits (ruling after r11). Inside one of its own entry's books
|
||||
// is book level, outside every book is universe level, and a draft's prefixes are all outside. Scope
|
||||
// checks prefixes first (scope.mjs), so a prefix equal to or holding a book would turn that book
|
||||
// into playground or supporting.
|
||||
const againstBooks = (owners, list, code) => {
|
||||
for (const [prefix, id] of owners) {
|
||||
for (const [book, owner] of bookOwner) {
|
||||
const at = `universes.${id}.${list} ${JSON.stringify(prefix)}`;
|
||||
if (prefix === `${book}/`) bad(`${at} is the whole book folder ${JSON.stringify(book)}`);
|
||||
const inside = holds(book, prefix), holding = holds(prefix, book);
|
||||
if ((inside || holding) && owner !== id)
|
||||
fail(code, `${where} ${at} ${inside ? 'lies inside' : 'holds'} book folder ${JSON.stringify(book)} of universes.${owner}`);
|
||||
if (holding) bad(`${at} holds its own book folder ${JSON.stringify(book)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
againstBooks(prefixOwner, 'playground', 'library-playground-other-universe');
|
||||
againstBooks(supportOwner, 'supporting', 'library-supporting-other-universe');
|
||||
// C2-01d, between the lists. A supporting prefix may equal a playground prefix of its own entry, and
|
||||
// a playground may hold one of its own entry's supporting prefixes. A supporting prefix that holds a
|
||||
// playground would hide it, since scope checks supporting first. Any overlap across entries would
|
||||
// give one path two universes.
|
||||
for (const [s, sid] of supportOwner) {
|
||||
for (const [p, pid] of prefixOwner) {
|
||||
const at = `universes.${sid}.supporting ${JSON.stringify(s)}`;
|
||||
if (sid !== pid && (s === p || holds(s, p) || holds(p, s)))
|
||||
bad(`${at} overlaps playground ${JSON.stringify(p)} of universes.${pid}`);
|
||||
if (sid === pid && s !== p && holds(s, p)) bad(`${at} holds playground ${JSON.stringify(p)}`);
|
||||
// into playground.
|
||||
for (const [prefix, id] of prefixOwner) {
|
||||
for (const [book, owner] of bookOwner) {
|
||||
const at = `universes.${id}.playground ${JSON.stringify(prefix)}`;
|
||||
if (prefix === `${book}/`) bad(`${at} is the whole book folder ${JSON.stringify(book)}`);
|
||||
const inside = holds(book, prefix), holding = holds(prefix, book);
|
||||
if ((inside || holding) && owner !== id)
|
||||
fail('library-playground-other-universe', `${where} ${at} ${inside ? 'lies inside' : 'holds'} book folder ${JSON.stringify(book)} of universes.${owner}`);
|
||||
if (holding) bad(`${at} holds its own book folder ${JSON.stringify(book)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +143,6 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
kind: 'library', remote: entry.remote, branch: entry.branch,
|
||||
...(Object.hasOwn(entry, 'user') ? { user: entry.user } : {}),
|
||||
...(Object.hasOwn(entry, 'vaultPrefix') ? { vaultPrefix: entry.vaultPrefix } : {}),
|
||||
...(Object.hasOwn(entry, 'editor') ? { editor: { name: entry.editor.name, email: entry.editor.email,
|
||||
...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {}) } } : {}),
|
||||
universes, types: { ...entry.types }, manuscript,
|
||||
});
|
||||
}
|
||||
|
||||
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user