Compare commits

..
Author SHA1 Message Date
jason.woltje bd5720662e row334: render P1 proxy system unit in Case 2 installs 2026-10-24 23:24:07 -05:00
32 changed files with 1428 additions and 666 deletions

No files matched your search

+4
View File
@@ -5585,3 +5585,7 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r
2026-10-24T23:37:46Z | Sage | Row 329 (#1827, P1 pinned model proxy, Magica) LANDED 61ebf2c34, pushed 23:31:27Z 10-24. Before: refactor at 669cdf42c. After: Magica's 4 commits (94cde633e, c65fb34ee, d1f6f2b19, 61ebf2c34) cherry-picked onto 669cdf42c, diff vs the gated tree limited to drift paths, range-diff in scratch. Gate: one unchanged full landing gate in /mnt/storage/scratch/sage-r329l/wt (gate.sh 8b4978887 c8a9bcf94), started 23:08:12Z at load 14.35, GATE GREEN 23:29:14Z at load 8.67, no rerun, no test or timeout edit (mos-1634). Host note: two Codex workers cleaned T3 worktrees from about 23:14Z (mos-1639, Vikunja 632). Approvals: Dewey r2 c31643 rev 4047, Boyd r2 c31642 rev 4048. #1827 comment 31650, closed. Vikunja 237 comment 1109 (stays open for P2-P7); C4 P10 red filed as Vikunja 631. Queue: note rev 4063, done rev 4064, committed b24d917ec. DEFERRED: N2 and R2-N1 bound to row 331, N1 bound to P7. Overlap: row 284 (packages/bus/src/broker.mjs) and row 157 (scripts/mosaic-config.mjs) rebase onto 61ebf2c34 before their gates.
2026-10-25T00:15:48Z | Sage | Row 284 (#1789, C2-05c library.get scope, Della) LANDED ce0b6ed41, pushed 00:09:52Z 10-25. Before: refactor at 9e2689dff (row 329 landing docs). After: fast-forward to Della's 7 rebased commits 78b932feb..ce0b6ed41; 8 paths, manifest-r4 de505cfd matches the landed tree. Gate: one unchanged full landing gate in /mnt/storage/scratch/sage-r284l/wt (the 329 gate.sh with D and the compose project renamed), started 23:51:19Z at load 1.62, GATE GREEN 00:09:04Z at load 5.20, no rerun, no test or timeout edit. Counts: bus 1020/0, business 283/0, cli 999 pass 1 allowed skip (FED R7), harness 177/0, library 156/0, seat 218/0, webui 458/0, every scripts/test-*.sh rc 0; queue 34 and task 87/93 skips are the standing ones. Della's whole-file runs on ce0b6ed41: library-lookups 12/0, library-tools 5/0, query 34/0. Approval: Webby round 2 delta c31654 rev 4070 (round 1 rev 4062). Overlap sweep: rows 13, 157, 309, gyro/row310 and fethry/row332-p1 clean. #1789 comment 31655, closed. No Vikunja task for the row. Queue: note rev 4071, done rev 4072, committed 7c8dc2033. DEFERRED: Webby N1 (whole-tree listing per get) bound to the next library read-path row; N2 needs nothing; N3 is this gate.
2026-10-25T00:18:35Z | Sage | Follow-up tasks for mos-1647 item 3 (Q109): Vikunja 633 (P7 cutover, CLIProxyAPI 8.0.42 startup Grok-npm and antigravity version refreshes, from row 332 isolated runs) and 634 (row 284 Webby N1, library get lists the whole tree). DEFERRED gains the 633 entry and the 634 number for row 284 N1. Filing script and receipt in agents/sage/work/vikunja-setup/.
2026-10-25 — Rockerduck row334 (#1832), before: Case 2 renders bus/Console only. Plan: reuse P1 proxy template/runner under --proxy, enforce three-unit re-install checks and proxy→bus→Console starts, cover refusals and both named mutants. Tests held for Sage CLEAR; design in agents/rockerduck/work/queue-334/BUILD.md.
2026-10-25 — Rockerduck row334 (#1832), RED evidence: whole case2-unit.test.mjs against baseline has 48 pass/33 expected new fail; whole case2-install.test.mjs has 27 pass/1 expected new fail. All 74 older tests pass. Implementations restored. Sage sage-4390 authorizes seat-branch candidate commit/push before GREEN; GREEN/mutants held until separate CLEAR after row331. Per-failure evidence: agents/rockerduck/work/queue-334/red-results.txt.
-187
View File
@@ -1,187 +0,0 @@
# Row 267 (C2-15, #1772): hybrid universes, exclusions, pins and the crossover. Build notes
Morgana, 2026-10-25. Brief: `docs/plans/2026-10-20_case2-rows.md` "C2-15 hybrid universes: exclusions,
pins and the end-to-end crossover" (blob 85e6fe2e). Reviewer Filbert, gate owner Sage. Claimed at rev 4087.
Branch `morgana/row267` from refactor 78b69e1e6. Design 1.4 is `agents/gosalyn/work/case2/DESIGN.md`.
Status: build committed under sage-4389 and sage-4407. Mos approved the survey.test.mjs:75 change (mos-1667,
read against 5b2da2203). Nothing is run. No runs before Sage's CLEAR.
## 1. What the code does today (78b69e1e6)
- `mapping.mjs:119-134` checks only the shape of a constraint. `exclude` is any folder path, and
`pin.entity` is any folder path with a scalar value. Nothing checks either against the sources.
- `query.mjs:52-57` `sees()` reads `exclude` as a path prefix only (`inside(c.exclude, file.path)`). The
type form `second/Characters` (`mapping.test.mjs:203`) passes the validator but hides nothing, because
no file path starts with `second/`.
- `reader.mjs:206-210` gives R6 only for a canon link into a universe that is neither the file's own nor
one of its sources. A hybrid canon file that links into an excluded source path gets no problem.
- `guard.mjs:91-97` lets any mix of one hybrid and its sources pass. It doesn't check exclusions.
- `problems.mjs` has R1-R7 and no R8.
## 2. Design
### 2.1 Validator (`mapping.mjs`)
The validator runs after every universe is read, alongside the existing sources check at `:141-146`,
because it needs each source's books, playground and supporting lists. For each constraint of a hybrid:
- **Type form:** the first segment is one of the hybrid's `sources`, and the rest is exactly one key of
`types`.
- **Path form:** the path equals, or lies inside, a book folder of one of the sources.
- Both forms fit: refuse `library-mapping-invalid` ("... reads as both a source/type and a source path").
- Neither form fits: refuse `library-mapping-invalid` ("... lies inside no book of the hybrid's
sources"). This covers a path in the hybrid's own books, in a non-source universe, or unmapped.
- **Path form under a playground:** a path equal to or inside one of a source's playground prefixes
refuses ("... lies under a playground of universes.X, which the hybrid never reads"). "Under" means the
path equals the prefix without its trailing `/`, or starts with the prefix.
- **`pin.entity`:** the same path-form rules apply, with no type form. An entity is one file.
The stored shape stays `{ exclude }` / `{ pin }`. I don't add a `form` field. `library_universe`
returns `constraints` as they are now, and the index cache digest doesn't change for existing mappings.
`excluded()` works out the form again each time. It's pure and cheap.
### 2.2 `excluded(mapping, hybrid, path)` (`scope.mjs`)
This returns true when any `exclude` of universe `hybrid` covers `path`:
- **Type form `S/T`:** `path` starts with `${book}/${T}/` for some book of source S.
- **Path form P:** `path === P` or `path` starts with `${P}/`.
It returns false when `hybrid` isn't a hybrid. It reads only the path (D2). It has three callers:
- `query.mjs` `sees()` replaces its own `inside(c.exclude, ...)` reading with this helper.
- `reader.mjs` uses it for the excluded-link R6.
- `guard.mjs` uses it for the excluded cite.
`query-fixture.mjs` `EXCLUSION` (`Book One/Research`) is path form and still fits, so the existing
C2-05a query tests keep their constants.
### 2.3 Reader: excluded-link R6 and R8
- **Excluded-link R6:** in the canon branch at `:206`, a hybrid file's link to a target where
`excluded(mapping, file.universe, to.path)` is true gives `problem('R6', file.path, link.line)`, with
the default message. A custom message isn't possible: `mirror.mjs:196-201` `validProblem` accepts a
sync-state problem only when `message === RULES[code]`, so a new message would make every later sync
state read refuse `library-state-invalid`. The link stays in `file.links`, as the other R-rules leave
it. `sees()` already drops it from the hybrid's `links_out`. A link that is both foreign and excluded
can't occur, because an excluded path always lies in a source, so each link gets at most one R6.
- **R8:** a new `RULES.R8` entry reads "pinned value changed". The check runs once per commit, after the
link pass, for each pin of each hybrid:
- **Entity missing** from `byPath`: R8 at `entity`, with no line. This covers a file that is absent,
R1, R2 or R7.
- **Entity present:** R8 at `entity`, on the `field` key's line where there is one, unless the parsed
front matter holds `field` as a scalar (string, number or boolean) and `String(parsed) ===
String(value)`. A list, null, a missing key and unparsed (R3) front matter all count as changed. Under
a plain `String()` comparison, `['grey']` would equal `'grey'`, so this rule closes that hole.
- **Hybrid versions:** each canon file of the hybrid whose `type` (from `classify`) equals the entity's
type, and whose name matches the entity's, is a version of it. Names are the trimmed front matter
`name` on both sides. When either file has no text `name`, the file names (stems) compare instead.
A version that holds `field` with another value gets R8 at its own path, on that key's line. A version
without `field` gets nothing, and neither does any other file that only links to the entity.
- The entity's type and name come from `classify(mapping, entity)` and the entity's parsed file. When the
entity is missing, its stem stands in for the name.
- An entity outside any type folder (a book root file, a manuscript file, or a folder that isn't a type)
has type null or `manuscript`. Its versions are then the hybrid canon files with that same type and a
matching name or stem. The brief doesn't cover this case, and no special rule is added for it (Filbert N2).
- R8 entries are deduplicated by path and line, so two pins or two hybrids on one missing or changed
entity give one entry (Filbert N1). Survey counts and the sync state count each path and line once.
### 2.4 Guard (`guard.mjs`)
After the existing span check at `:91-97`, the guard looks for a hybrid among the cited universes. If
there is one, and some cite lies in one of its sources while `excluded(mapping, hybrid, c.path)` is true,
the guard refuses `citation-cross-universe` ("citation N (path) is excluded from hybrid H"). A reply that
cites only source paths, excluded ones included, has no hybrid among its universes, so it's unchanged.
### 2.5 Setup sheet
`docs/case2/SETUP-SHEET.md` gets one section after "Supporting folders", headed "Making a crossover
(hybrid) universe". It covers `kind`, `name`, `sources`, `books`, `playground`, `constraints`
(`exclude` in both forms, `pin`) and `notes`, and says that the continuity role reads and cites the notes.
It also says that Jason edits this entry by hand and agents can't change it (C2-04 rule M9), as the
sheet already says for the rest of the entry.
## 3. Tests and mutants
New files and cases, all on scratch repos under TMPDIR (never /tmp):
- **`packages/library/tests/hybrid.test.mjs` (new):**
- A business-file-shaped mapping (fixture `mapping.json` plus a `cross` hybrid) on a repo in her
layout's shape.
- `find` and `search` in `cross` return its own files plus the sources' canon, each carrying its source
universe. They never return a source playground or an excluded path, and this runs once for each
exclude form.
- `get` keeps `links_out` into the sources and drops excluded targets.
- A source universe's lookups never return a `cross` file.
- `universeInfo` returns the sources, constraints and notes.
- `excluded()` gets unit cases for both forms and for a non-hybrid.
- **`mapping.test.mjs`:** one case per new refusal: both forms fit, neither fits, the hybrid's own book,
an exclude under a source playground, a pin under a source playground, and a `pin.entity` that doesn't
end in `.md` (Q5). `second/Characters` at `:203` still checks.
- **`reader.test.mjs`:**
- R8 cases: entity missing, two pins on a missing entity give one entry (N1), entity changed, a same-name hybrid version with no link, and a list value.
- Non-R8 cases: a hybrid place file with `age: 300` that links the entity, and a different hybrid
character that links it.
- The excluded-link R6, once for each exclude form.
- **`guard.test.mjs`:** a hybrid cite with source canon passes, and a hybrid cite beside an excluded path
refuses in both forms. A cite of only an excluded source path passes as that universe's item.
- **`packages/bus/tests/citation-guard.test.mjs`:** through `citationWorld` with `mapChange` adding the
hybrid, the excluded-beside-hybrid send refuses and the `messages` count stays the same.
- **Sync state after an excluded-link R6 (sage-4389 finding a):** one case that syncs a commit holding a
hybrid file's excluded link, then reads the sync state back with `readSyncState` and finds the R6 with
the default message. This shows `validProblem` (`mirror.mjs:196-201`) still accepts the state.
- **`packages/bus/tests/library-lookups.test.mjs`:** one broker case, where `library.find` and
`library.universe` in the hybrid return what the library tests above assert.
- **C2-04 M9 rule (agents can't write `projects`), not a mutant:** a case in `packages/business/tests/library-projects-protected.test.mjs` where an organization
write that adds a new hybrid universe refuses `organization-projects-protected`. This is outside the
brief's "tests in library and bus" list. Sage ruled it in (Q1), so the business suite is on the run list.
- **`docs/case2/SURVEY-RUN.md:98`** names the problems line as `R1 .., ..., R8 ..`. No test reads that file.
- **One existing assertion changes:** `survey.test.mjs:75` gains `, R8 0`. The before and after lines are
in `assertion-diff.md`, which goes to Mos before the commit (mos-1272).
Mutants. Each runs only its own named tests, and the tree is restored after each one.
| # | Mutation | Killed by |
|---|---|---|
| X1 | `excluded()` ignores the type form | hybrid.test "C2-15 excluded(): ..." and "C2-15 type form: find and search ..." |
| X2 | `excluded()` ignores the path form | hybrid.test "C2-15 excluded(): ..." and "C2-15 path form: find and search ..." |
| X3 | `sees()` keeps its old `inside()` reading | hybrid.test "C2-15 type form: find and search ..." |
| X4 | the validator accepts "both forms fit" | mapping.test "C2-15: each exclude and pin is checked against the hybrid's sources" |
| X5 | the validator skips the playground check | mapping.test "C2-15: each exclude and pin is checked against the hybrid's sources" |
| X6 | R8 compares with plain `String()` (list equals scalar) | reader.test "C2-15 R8: the pinned entity reads clean ..." (list case) |
| X7 | a version is matched by link instead of name | reader.test "C2-15 R8: a hybrid version of the entity ..." |
| X8 | the reader drops the excluded-link R6 | reader.test "C2-15 R6: a hybrid file that links into an excluded source path ..."; sync.test "C2-15 sage-4389 (a) ..." also fails |
| X9 | the guard skips the excluded check | guard.test "C2-15 a hybrid cite beside an excluded source path refuses citation-cross-universe, both forms" (the mutant run); bus citation-guard "C2-15 excluded cite beside a hybrid refuses and inserts nothing" also fails |
## 4. Rulings (sage-4389)
- **Q1 yes.** The C2-04 M9 case is new and goes only in `packages/business/tests/library-projects-protected.test.mjs`.
No existing assertion there changes. The business suite is on the run list.
- **Q2 dropped.** The condition was a file:line showing that the hybrid's index, reader and guard never
read a source's supporting prefix. Two of the three do read it:
- Index: `query.mjs:55` `sees()` returns false for a source file whose scope isn't `canon`, so the
hybrid's lookups never show a source's supporting file. This one holds.
- Reader: `reader.mjs:206-209` checks R5 for playground targets only, and R6 skips any target in a
source universe. A hybrid canon file that links into a source's supporting file gets no problem and
keeps the link (`:211`).
- Guard: `guard.mjs:91-97` checks the cited universes and never their scope. A reply that cites a
hybrid file beside a source's supporting file passes, and so does one beside a source's playground
file (`guard.mjs:87-90` only stops a playground cite marked `as: 'canon'`).
Sage's ranges in sage-4399 (`reader.mjs:206-211`, `guard.mjs:87-97`) cover the same lines. Sage files
a DEFERRED entry for the guard and reader gap at landing.
So the validator does not refuse an exclude under a source's supporting prefix. The guard observation
is outside this row's guard scope ("the excluded-path cite only"); I report it to Sage as a possible
follow-up and don't change it here.
- **Q3:** R8 reaches the survey and the sync state as described. `survey.test.mjs:75` goes through
`assertion-diff.md`. Finding (a) adds the sync-state readback test in §3. `packages/library/README.md`
lists R1-R7 in its problems table, so it gains an R8 row; no test reads that file.
- **Q4 as written:** versions count only in the hybrid's canon.
- **Q5 yes:** a `pin.entity` that doesn't end in `.md` refuses `library-mapping-invalid`, with its own test.
- **Finding (a):** R6 keeps the default message, with the readback test above.
## 5. Gate
Filbert approves the manifest. The library and bus suites, plus business if Q1 stands, run green on
Sage's gate along with every `scripts/test-*.sh`. Hash pins on the changed files are grepped before
in-review.
@@ -1,22 +0,0 @@
# Row 267: one existing assertion changes (mos-1272)
File: `packages/library/tests/survey.test.mjs:75`, in the test that checks the survey's last three lines
with `--mapping`.
Before:
```
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0']);
```
After:
```
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0, R8 0']);
```
Why: R8 ("pinned value changed") is a new rule in `problems.mjs` `RULES`, and the survey line lists a
count for every `RULES` key (`survey.mjs:70`), so it gains `R8 0`. The fixture has no hybrid and no pin,
so the count is 0 and no other count moves.
No other existing assertion in the row changes.
+102
View File
@@ -0,0 +1,102 @@
# Row 334 — Case 2 proxy unit (#1832)
## Design at 78b69e1e6 (2026-10-25)
Scope: mos-1616 ruling 1, P1 item 7 only. No template, runner, PIN,
user-unit path, config/secret access, or live unit changes.
Current references: case2-unit.sh template seams 44-45, defaults 54-57,
render 435, check_unit 489, render checks 548-551, re-install 553-573,
active-state capture 576-580, writes/start/report 583-610.
case2-install.sh Next: 231. P1 instance.sh PROXY_UNIT 86;
bus-service.sh dependency insertion 93. P1 template ExecStart 13;
run.mjs validates config 11-13 and calls startVerifiedProxy 14;
runner.mjs refuses proxy-not-configured 8, verifyInstall 9;
settings.mjs verifyInstall 23-30 refuses proxy-install-invalid for a hash
mismatch. run.mjs catches that and exits 3. P1 item 7 is rows doc 86-90;
row209 entry check 2 remains 32-36.
Add bare --proxy and CASE2_PROXY_TEMPLATE. Render P1's unmodified template
with copy, PATH, node and deleted CONFIG_ENV; add User/Group plus bus's four
hardening lines, no capabilities or EnvironmentFile. check_unit proxy uses
exact node + copy/packages/model-proxy/src/run.mjs, NoNewPrivileges=yes,
and every common refusal. Restrict CLAUDE_ENV expectation and capabilities
to their existing bus/webui kinds. Insert bus Wants/After immediately after
Description exactly as P1 does, only with --proxy. No-proxy output remains
byte identical.
Re-install checks all selected units against render modulo copy id and
compares every installed copy id. Existing proxy without --proxy refuses
before writes, including a dangling symlink. No deletion. Enable all
selected units, then start proxy, bus, Console. Capture/report already-running
proxy with the others and name all three in restart-together guidance.
Change only case2-install.sh's closing Next: to tell root to add --proxy
when the config has a proxy section.
Tests first, unrun until Sage CLEAR: byte-identical non-proxy renders;
proxy content and exact dependencies; each common/proxy check_unit refusal
by message; rendered ExecStart wrong-PIN refusal proxy-install-invalid rc 3;
three-unit re-install and mixed copies including proxy-only mismatch;
installed proxy without flag preserves files; start order and failures;
already-running report; install Next: guidance. Named mutants: remove bus
After=; direct binary ExecStart bypassing run.mjs. Whole-file run list will
be derived from static imports before any run; case1-accept stays off.
P2 is not present on this base (row331 still in progress); inspect landed
P2 refusal code before final handoff. No flag/config false-positive check:
P1 startVerifiedProxy refuses proxy-not-configured, run.mjs exits 3.
## Slot / session
No tests run. CLEAR pending from Sage. Canonical queue start initially
refused: lock held by magica-331-start since 03:57:01.839Z. Same operation
will be retried after a minute; no lock manipulation.
Correction to line references above: row209 entry check 2 is 29-33;
run.mjs config validation 8-10, runner call 11, refusal exit status 18.
case2-unit.sh is byte-identical to brief base 58b2bbb3a (sha256
4ca8769134d83ee391bcc73af5bd042b9e2b161cac22821d29fcca710a9afad4).
P1 paths named above have no diff between 86cb5c221 and 78b69e1e6.
Own-worktree root npm ci --ignore-scripts --no-audit --no-fund: rc 0,
128 packages. No tests invoked.
Sage sage-4346 accepts design, authorizes writing RED tests then code while
holding all runs until CLEAR. Tests were written first, then implementation;
no RED run has occurred. Installed proxy without --proxy explicitly refuses
and preserves bytes; dangling symlink also refuses, never deletes. P2 code
remains pending until row331 lands. Both wrong-flag P1 runner paths covered.
Import-manifest correction: expanded traversal to include multiline import
and export-from edges (the business/library barrel modules). Closure is
53 local modules; only the same two test entrypoints occur. The run list
is unchanged; this is source inspection, no tests executed.
Queue start completed: rockerduck-334-start rev4079, row334
briefed→in-progress. git diff --check passed. Implementation, tests and
import manifest prepared; no tests, commits or pushes performed. Verification
awaits Sage's CLEAR; no live host/instance access occurred.
## RED run authorized by sage-4387, 2026-10-25 04:21Z
Whole files, sequential, with each corresponding script temporarily restored
from 78b69e1e6 and its implementation restored by EXIT trap afterwards:
- case2-unit.test.mjs: rc1, 81 tests, 48 pass, 33 fail, 0 skips;
TMPDIR=/mnt/storage/scratch/tmp/QD3vp5, nice=19, 77995.928687 ms.
All 47 old tests pass; new no-proxy byte equality passes. 32 new failures
encounter baseline unsupported --proxy usage rc4; dangling installed proxy
symlink case gets rc0 rather than refusal because baseline ignores it.
- case2-install.test.mjs: rc1, 28 tests, 27 pass, 1 fail, 0 skips;
TMPDIR=/mnt/storage/scratch/tmp/YAGfKJ, nice=19, 10016.247032 ms.
All old tests pass. New Next: guidance assertion fails because baseline
lacks add --proxy when the config has a proxy section.
Every failure and cause is listed in red-results.txt; full plain-text
outputs in red-unit.log and red-install.log. Absolute node/test paths used.
Implementation restored, git diff --check clean. No old-test red, rerun,
assertion change, GREEN or mutant run. P2 check still pending row331.
Slot returned to Sage; GREEN and mutants await separate CLEAR.
Sage sage-4390 accepts clean RED and explicitly authorizes committing and
pushing this candidate to rockerduck/row334 before GREEN. No force push.
GREEN and mutants remain held for a separate CLEAR after row331 lands;
then rebase onto landed P2 and check its missing-flag refusal code.
@@ -0,0 +1,69 @@
# Row334 whole-file run list (UNRUN; awaiting CLEAR)
Entrypoints are the brief-owned tests, which spawn the changed shell scripts.
Their import graph is recorded below; no imported module is itself a test entrypoint.
case1-accept.test.mjs is excluded (mos-1612).
packages/cli/tests/case2-unit.test.mjs -> packages/business/tests/helpers.mjs, packages/seat/src/sandbox.mjs, packages/cli/tests/helpers.mjs, packages/cli/tests/case-sudo-fixture.mjs
packages/business/tests/helpers.mjs -> (builtins/external only)
packages/seat/src/sandbox.mjs -> packages/business/src/vocabulary.mjs
packages/business/src/vocabulary.mjs -> (builtins/external only)
packages/cli/tests/helpers.mjs -> packages/bus/src/broker.mjs, packages/bus/src/store.mjs, packages/business/tests/helpers.mjs, packages/harness/tests/helpers.mjs, packages/plugins/tests/helpers.mjs, packages/cli/src/transport.mjs
packages/bus/src/broker.mjs -> packages/bus/src/address.mjs, packages/processing-control/src/state.mjs, packages/plugins/src/kinds.mjs, packages/tasks/src/requirements.mjs, packages/bus/src/landing.mjs, packages/business/src/vocabulary.mjs
packages/bus/src/address.mjs -> (builtins/external only)
packages/processing-control/src/state.mjs -> (builtins/external only)
packages/plugins/src/kinds.mjs -> (builtins/external only)
packages/tasks/src/requirements.mjs -> (builtins/external only)
packages/bus/src/landing.mjs -> (builtins/external only)
packages/bus/src/store.mjs -> (builtins/external only)
packages/harness/tests/helpers.mjs -> packages/business/src/index.mjs, packages/business/tests/helpers.mjs, packages/plugins/src/loader.mjs
packages/business/src/index.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs, packages/business/src/role.mjs, packages/business/src/vars.mjs, packages/business/src/credentials.mjs, packages/business/src/business.mjs, packages/business/src/access.mjs, packages/business/src/project.mjs, packages/business/src/resolve.mjs, packages/business/src/library-seats.mjs
packages/business/src/errors.mjs -> (builtins/external only)
packages/business/src/role.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs, packages/business/src/util.mjs
packages/business/src/util.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs
packages/business/src/vars.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs, packages/business/src/util.mjs, packages/plugins/src/kinds.mjs
packages/business/src/credentials.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs, packages/business/src/util.mjs
packages/business/src/business.mjs -> packages/business/src/errors.mjs, packages/business/src/launch.mjs, packages/business/src/vars.mjs, packages/business/src/credentials.mjs, packages/business/src/role.mjs, packages/business/src/vocabulary.mjs, packages/plugins/src/kinds.mjs, packages/library/src/mapping.mjs, packages/business/src/util.mjs
packages/business/src/launch.mjs -> packages/business/src/util.mjs, packages/business/src/errors.mjs
packages/library/src/mapping.mjs -> packages/library/src/problems.mjs
packages/library/src/problems.mjs -> (builtins/external only)
packages/business/src/access.mjs -> packages/business/src/errors.mjs, packages/business/src/business.mjs, packages/business/src/util.mjs, packages/business/src/vocabulary.mjs
packages/business/src/project.mjs -> packages/business/src/errors.mjs, packages/business/src/vars.mjs, packages/business/src/util.mjs
packages/business/src/resolve.mjs -> packages/business/src/errors.mjs, packages/business/src/vocabulary.mjs, packages/business/src/vars.mjs, packages/business/src/util.mjs, packages/business/src/project.mjs
packages/business/src/library-seats.mjs -> packages/business/src/errors.mjs, packages/business/src/business.mjs, packages/business/src/vocabulary.mjs
packages/plugins/src/loader.mjs -> packages/plugins/src/kinds.mjs
packages/plugins/tests/helpers.mjs -> (builtins/external only)
packages/cli/src/transport.mjs -> packages/cli/src/errors.mjs
packages/cli/src/errors.mjs -> (builtins/external only)
packages/cli/tests/case-sudo-fixture.mjs -> (builtins/external only)
packages/cli/tests/case2-install.test.mjs -> packages/business/tests/helpers.mjs, packages/library/tests/helpers.mjs, packages/library/src/index.mjs, packages/seat/src/checkout.mjs, packages/project/src/git.mjs, packages/cli/tests/helpers.mjs
packages/library/tests/helpers.mjs -> packages/library/src/mapping.mjs
packages/library/src/index.mjs -> packages/library/src/problems.mjs, packages/library/src/mapping.mjs, packages/library/src/scope.mjs, packages/library/src/supporting.mjs, packages/library/src/links.mjs, packages/library/src/reader.mjs, packages/library/src/render.mjs, packages/library/src/survey.mjs, packages/library/src/mirror.mjs, packages/library/src/sync.mjs, packages/library/src/index-build.mjs, packages/library/src/query.mjs, packages/library/src/limits.mjs, packages/library/src/guard.mjs
packages/library/src/scope.mjs -> (builtins/external only)
packages/library/src/supporting.mjs -> packages/library/src/problems.mjs, packages/library/src/scope.mjs
packages/library/src/links.mjs -> (builtins/external only)
packages/library/src/reader.mjs -> packages/project/src/git.mjs, packages/library/src/scope.mjs, packages/library/src/links.mjs, packages/library/src/problems.mjs
packages/project/src/git.mjs -> packages/seat/src/checkout.mjs, packages/project/src/paths.mjs, packages/processing-control/src/effects.mjs
packages/seat/src/checkout.mjs -> packages/processing-control/src/effects.mjs
packages/processing-control/src/effects.mjs -> packages/processing-control/src/state.mjs, packages/processing-control/src/runtimes.mjs
packages/processing-control/src/runtimes.mjs -> packages/processing-control/src/state.mjs
packages/project/src/paths.mjs -> (builtins/external only)
packages/library/src/render.mjs -> (builtins/external only)
packages/library/src/survey.mjs -> packages/library/src/mapping.mjs, packages/library/src/scope.mjs, packages/library/src/links.mjs, packages/library/src/reader.mjs, packages/library/src/problems.mjs
packages/library/src/mirror.mjs -> packages/project/src/paths.mjs, packages/project/src/git.mjs, packages/library/src/mapping.mjs, packages/library/src/scope.mjs, packages/library/src/problems.mjs, packages/library/src/reader.mjs
packages/library/src/sync.mjs -> packages/seat/src/checkout.mjs, packages/project/src/git.mjs, packages/library/src/mapping.mjs, packages/library/src/problems.mjs, packages/library/src/reader.mjs, packages/library/src/mirror.mjs
packages/library/src/index-build.mjs -> packages/library/src/reader.mjs, packages/library/src/problems.mjs
packages/library/src/query.mjs -> packages/library/src/problems.mjs, packages/library/src/mapping.mjs, packages/library/src/index-build.mjs, packages/library/src/reader.mjs
packages/library/src/limits.mjs -> (builtins/external only)
packages/library/src/guard.mjs -> packages/project/src/git.mjs, packages/library/src/scope.mjs, packages/library/src/reader.mjs, packages/library/src/problems.mjs, packages/library/src/mirror.mjs, packages/library/src/limits.mjs, packages/library/src/reply-protocol.mjs
packages/library/src/reply-protocol.mjs -> (builtins/external only)
Whole-file commands after CLEAR (one fresh shell-mktemp TMPDIR each; nice=19):
node --test packages/cli/tests/case2-unit.test.mjs
node --test packages/cli/tests/case2-install.test.mjs
Before these: new row334 tests only on saved 78b69e1e6 scripts for RED,
then restored implementation. After GREEN: named mutants individually,
each with its directly relevant row334 test. No scripts/test-*.sh builder run planned.
Includes multiline imports and export-from module edges: 53 local modules.
@@ -0,0 +1,68 @@
TMPDIR=/mnt/storage/scratch/tmp/YAGfKJ nice=19
✔ the library install writes the business file, personas and slot dir, makes the copy and starts nothing (386.113555ms)
✔ a library re-run passes and leaves every file as it was (496.225906ms)
✔ a re-run refuses business-file-differs and leaves the file unchanged (376.395513ms)
✔ a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing (398.216221ms)
✔ the factory install writes no library mapping, persona or slot dir (247.905208ms)
✔ the factory install refuses a library project and bus.tester off, before anything is written (105.176208ms)
✔ the library install refuses bus.tester on and a persona var in the input (104.503599ms)
✔ the install refuses root, a business id mismatch, another --home and a config dir with group bits (80.144703ms)
✔ the library install needs --personas (7.831929ms)
✔ a validate refusal removes the business file this run wrote (311.701322ms)
✔ N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte (537.848604ms)
✔ a clean library host passes the precheck with no mirror yet (509.009214ms)
✔ the library precheck refuses a persona file at 0644 (506.697677ms)
✔ the library precheck refuses a persona whose digest isn't persona.sha256 (495.113108ms)
✔ the library precheck refuses a persona dir with group bits (481.900638ms)
✔ the library precheck refuses a business file at 0644 (462.626038ms)
✔ the library precheck refuses a changes.jsonl at 0644 (687.490332ms)
✔ the library precheck refuses a Discord binding (499.568905ms)
✔ --library-status refuses library-mirror-missing before the bus has made the mirror (377.717622ms)
✔ --library-status refuses library-never-synced on a fresh mirror (410.956236ms)
✔ --library-status names a recorded lastError before the null lastFetch it leaves (N3) (417.888767ms)
✔ --library-status prints each R-problem's path as an escaped JSON string, then the count, and passes (481.141014ms)
✔ --library-status takes no --profile (6.246241ms)
✔ Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent (560.431267ms)
✔ row 322 F8: library personas install from the copy with no clone node_modules (183.399079ms)
✔ row 322 F9: importer refusal keeps the error line rather than the Node banner (118.731885ms)
✔ C2-21: library install supplies chief identity and books publish for its real read grant (330.48101ms)
✖ row334 install Next tells root to select proxy only when configured (302.965779ms)
ℹ tests 28
ℹ suites 0
ℹ pass 27
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10016.247032
✖ failing tests:
test at packages/cli/tests/case2-install.test.mjs:495:1
✖ row334 install Next tells root to select proxy only when configured (302.965779ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /Next: root runs case2-unit.sh[^\n]+add --proxy when the config has a proxy section/. Input:
'case2-install: config /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/config.json\n' +
'case2-install: copy /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.local/share/mosaic-stack/copies/cccccccccccccccccccccccccccccccccccccccc\n' +
'case2-install: personas installed under /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/personas/acme\n' +
'case2-install: business file /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/businesses/acme.json written\n' +
'case2-install: slot dir /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/secrets/acme\n' +
'case2-install: business acme validates\n' +
'case2-install: done. Next: root runs case2-unit.sh --copy cccccccccccccccccccccccccccccccccccccccc --business acme (design 10 step 2c), then 2d and 3.\n' +
'case2-install: Then Jason places the library token in /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/secrets/acme (step 4), and <user> runs case2-precheck.sh --business acme --library-status (step 4a).\n'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-install.test.mjs:498:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'case2-install: config /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/config.json\ncase2-install: copy /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.local/share/mosaic-stack/copies/cccccccccccccccccccccccccccccccccccccccc\ncase2-install: personas installed under /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/personas/acme\ncase2-install: business file /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/businesses/acme.json written\ncase2-install: slot dir /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/secrets/acme\ncase2-install: business acme validates\ncase2-install: done. Next: root runs case2-unit.sh --copy cccccccccccccccccccccccccccccccccccccccc --business acme (design 10 step 2c), then 2d and 3.\ncase2-install: Then Jason places the library token in /mnt/storage/scratch/tmp/YAGfKJ/case2-install-lXA9Qd/home/.config/mosaic-dev/secrets/acme (step 4), and <user> runs case2-precheck.sh --business acme --library-status (step 4a).\n',
expected: /Next: root runs case2-unit.sh[^\n]+add --proxy when the config has a proxy section/,
operator: 'match',
diff: 'simple'
}
@@ -0,0 +1,52 @@
# RED results — sage-4387 (2026-10-25)
Baseline scripts: 78b69e1e6. Current row334 tests. Whole files, sequential.
Both implementation scripts restored via EXIT traps after their run.
No GREEN or mutant run. No old-test failure. No assertion changed.
case2-unit.test.mjs: exit 1; 81 tests, 48 pass, 33 fail, 0 skip.
47 existing tests plus new byte-identical no-proxy test pass.
TMPDIR=/mnt/storage/scratch/tmp/QD3vp5; nice=19; duration 77995.928687 ms.
Every unit failure and its observed reason:
- row334 proxy uses P1 runner, bus dependencies and proxy bus Console start order: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses continuation by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses not-setting by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses RestrictNamespaces by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses PrivateUsers by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses SystemCallFilter by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses PermissionsStartOnly by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses EnvironmentFile by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses AmbientCapabilities by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses CapabilityBoundingSet by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses privileged by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses direct-binary by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses nnp by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses cwd by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses environment by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses install by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses user by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses group by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses ProtectSystem by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses ReadWritePaths by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses ProtectHome by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses PrivateTmp by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy refuses token by message before writing: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy template missing or symlink refuses: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 rendered ExecStart refuses wrong PIN and missing proxy with P1 status: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 reinstall repins all three and refuses a proxy copy mismatch: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 installed proxy without --proxy refuses and preserves all bytes: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 dangling proxy symlink without flag refuses without deleting: baseline ignores installed proxy symlink without flag, returns 0 instead of refusal 1.
- row334 active proxy reports old copy and all three must restart together: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 failed mosaic-proxy.service start leaves later units unstarted: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 failed [email protected] start leaves later units unstarted: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 installed proxy enforces hardening order and remaining common refusals: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
- row334 proxy installed path rejects symlink and non-regular file: baseline does not parse --proxy: usage exit 4, before the proxy-specific assertion/setup can be reached.
case2-install.test.mjs: exit 1; 28 tests, 27 pass, 1 fail, 0 skip.
TMPDIR=/mnt/storage/scratch/tmp/YAGfKJ; nice=19; duration 10016.247032 ms.
- row334 install Next tells root to select proxy only when configured: baseline Next: lacks add --proxy when the config has a proxy section.
Commands used absolute /usr/bin/node --test and absolute test-file paths.
Full plain-text outputs: red-unit.log and red-install.log beside this file.
P2 refusal-code check remains pending row331. Slot returned to Sage.
@@ -0,0 +1,873 @@
TMPDIR=/mnt/storage/scratch/tmp/QD3vp5 nice=19
✔ row253 factory socket uses actual config and target-uid query without widening hardening (351.144059ms)
✔ row253 installer refuses unsafe location and query facts before unit writes (3621.94316ms)
✔ row253 query timeout kills its process group and leaves no running child (7054.318085ms)
✔ row253 protected homes refuse before any filesystem access (9.291102ms)
✔ row253 C4 sleep query has exact inner reason and cleans all members (10796.380077ms)
✔ row253 C4 signals query has exact inner reason and cleans all members (10819.227773ms)
✔ row253 C4 member query has exact inner reason and cleans all members (10897.328548ms)
✔ row253 C4 overflow is output-limit in both sudo modes (554.12811ms)
✔ row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup (14201.797743ms)
✔ row253 C4 inline supervisor is byte-identical in both host scripts (0.888234ms)
✔ row253 C4 cleanup budget starts after inventory pass one (46.615614ms)
✔ row253 query reaps its group even when a child closes stdout before the query returns (152.118314ms)
✔ row253 config parser is strict (11.560624ms)
✔ a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console (120.676804ms)
✔ PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node (131.630623ms)
✔ a failed bus start refuses with the Console not started (123.177864ms)
✔ the script never links a unit file (0.18683ms)
✔ factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty (746.075081ms)
✔ a re-install over running units says they are still on the old copy and must restart together, never that it started them (494.63679ms)
✔ a re-install to another copy re-renders both units, and an edited installed unit refuses (1175.719345ms)
✔ a template with a namespace, user or system call restriction refuses (335.625113ms)
✔ the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none (214.600182ms)
✔ a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses (151.081813ms)
✔ a symlink at either unit path refuses (203.555513ms)
✔ a --node that is relative, not root-owned or under <home> refuses (27.297759ms)
✔ a re-install with a different --node or different serve flags refuses (599.873393ms)
✔ an installed pair whose units name different copies refuses (328.321083ms)
✔ a copy id that is short, uppercase, holds / or .., or names no directory refuses (46.028703ms)
✔ library refuses a privileged-floor miss, a bad proof pair and every flag outside its set (344.833107ms)
✔ library refuses a proof file not owned by <user>'s uid (18.013172ms)
✔ factory refuses duplicate socket, a missing --port or --business, and each proxy flag (209.757808ms)
✔ row253 factory refuses retired TCP and a value after the socket marker (59.218628ms)
✔ factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude (1686.680735ms)
✔ the precheck passes on a good host and prints free memory, without the key (226.963557ms)
✔ the precheck refuses each failed host check (1765.731095ms)
✔ the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root (927.260864ms)
✔ a bwrap that fails refuses and prints the sysctl and where bwrap is (200.731617ms)
✔ an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit (1055.360802ms)
✔ bus.tester: library refuses it on, factory needs it on, and factory refuses a library business (927.574539ms)
✔ the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong (1694.074713ms)
✔ the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH (498.707509ms)
✔ the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line (1155.959127ms)
✔ inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds (106.466066ms)
✔ Vikunja 614 factory: scoped @anthropic-ai Claude path renders and passes installed read-back (1024.721235ms)
✔ Vikunja 614 factory: plain paths still refuse every named unsafe character (117.946806ms)
✔ Vikunja 614 precheck: explicit null and named notifier decisions pass (384.090317ms)
✔ Vikunja 614 precheck: notifier config refuses missing, public, malformed and extra-key decisions (1596.303308ms)
✔ row334 no-proxy renders stay byte-identical to the pre-row renderer (316.417263ms)
✖ row334 proxy uses P1 runner, bus dependencies and proxy bus Console start order (6.436602ms)
✖ row334 proxy refuses continuation by message before writing (6.291734ms)
✖ row334 proxy refuses not-setting by message before writing (5.588449ms)
✖ row334 proxy refuses RestrictNamespaces by message before writing (5.71389ms)
✖ row334 proxy refuses PrivateUsers by message before writing (5.34285ms)
✖ row334 proxy refuses SystemCallFilter by message before writing (5.400994ms)
✖ row334 proxy refuses PermissionsStartOnly by message before writing (5.443023ms)
✖ row334 proxy refuses EnvironmentFile by message before writing (5.052075ms)
✖ row334 proxy refuses AmbientCapabilities by message before writing (5.027453ms)
✖ row334 proxy refuses CapabilityBoundingSet by message before writing (5.604853ms)
✖ row334 proxy refuses privileged by message before writing (5.176325ms)
✖ row334 proxy refuses direct-binary by message before writing (5.301058ms)
✖ row334 proxy refuses nnp by message before writing (4.978268ms)
✖ row334 proxy refuses cwd by message before writing (4.903056ms)
✖ row334 proxy refuses environment by message before writing (4.928207ms)
✖ row334 proxy refuses install by message before writing (4.935225ms)
✖ row334 proxy refuses user by message before writing (4.996881ms)
✖ row334 proxy refuses group by message before writing (4.740792ms)
✖ row334 proxy refuses ProtectSystem by message before writing (7.412574ms)
✖ row334 proxy refuses ReadWritePaths by message before writing (5.039647ms)
✖ row334 proxy refuses ProtectHome by message before writing (4.948113ms)
✖ row334 proxy refuses PrivateTmp by message before writing (5.049564ms)
✖ row334 proxy refuses token by message before writing (5.291145ms)
✖ row334 proxy template missing or symlink refuses (4.77612ms)
✖ row334 rendered ExecStart refuses wrong PIN and missing proxy with P1 status (5.907949ms)
✖ row334 reinstall repins all three and refuses a proxy copy mismatch (5.706372ms)
✖ row334 installed proxy without --proxy refuses and preserves all bytes (4.919106ms)
✖ row334 dangling proxy symlink without flag refuses without deleting (127.81539ms)
✖ row334 active proxy reports old copy and all three must restart together (5.181396ms)
✖ row334 failed mosaic-proxy.service start leaves later units unstarted (4.827066ms)
✖ row334 failed [email protected] start leaves later units unstarted (4.558348ms)
✖ row334 installed proxy enforces hardening order and remaining common refusals (5.170683ms)
✖ row334 proxy installed path rejects symlink and non-regular file (4.685001ms)
ℹ tests 81
ℹ suites 0
ℹ pass 48
ℹ fail 33
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77995.928687
✖ failing tests:
test at packages/cli/tests/case2-unit.test.mjs:966:1
✖ row334 proxy uses P1 runner, bus dependencies and proxy bus Console start order (6.436602ms)
AssertionError [ERR_ASSERTION]: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:969:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses continuation by message before writing (6.291734ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses not-setting by message before writing (5.588449ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses RestrictNamespaces by message before writing (5.71389ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses PrivateUsers by message before writing (5.34285ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses SystemCallFilter by message before writing (5.400994ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses PermissionsStartOnly by message before writing (5.443023ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses EnvironmentFile by message before writing (5.052075ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses AmbientCapabilities by message before writing (5.027453ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses CapabilityBoundingSet by message before writing (5.604853ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses privileged by message before writing (5.176325ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses direct-binary by message before writing (5.301058ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses nnp by message before writing (4.978268ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses cwd by message before writing (4.903056ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses environment by message before writing (4.928207ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses install by message before writing (4.935225ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses user by message before writing (4.996881ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses group by message before writing (4.740792ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses ProtectSystem by message before writing (7.412574ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses ReadWritePaths by message before writing (5.039647ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses ProtectHome by message before writing (4.948113ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses PrivateTmp by message before writing (5.049564ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1004:52
✖ row334 proxy refuses token by message before writing (5.291145ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1007:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1012:1
✖ row334 proxy template missing or symlink refuses (4.77612ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1014:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1019:1
✖ row334 rendered ExecStart refuses wrong PIN and missing proxy with P1 status (5.907949ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1021:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1044:1
✖ row334 reinstall repins all three and refuses a proxy copy mismatch (5.706372ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1046:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1055:1
✖ row334 installed proxy without --proxy refuses and preserves all bytes (4.919106ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1057:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1065:1
✖ row334 dangling proxy symlink without flag refuses without deleting (127.81539ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 0: case2-unit: wrote /mnt/storage/scratch/tmp/QD3vp5/case2-unit-h4V4Zn/units/[email protected] and /mnt/storage/scratch/tmp/QD3vp5/case2-unit-h4V4Zn/units/mosaic-webui.service from copy aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; enabled and started [email protected], then mosaic-webui.service
0 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1068:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1072:1
✖ row334 active proxy reports old copy and all three must restart together (5.181396ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1074:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1080:73
✖ row334 failed mosaic-proxy.service start leaves later units unstarted (4.827066ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1082:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1080:73
✖ row334 failed [email protected] start leaves later units unstarted (4.558348ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1082:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1088:1
✖ row334 installed proxy enforces hardening order and remaining common refusals (5.170683ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1090:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/case2-unit.test.mjs:1103:1
✖ row334 proxy installed path rejects symlink and non-regular file (4.685001ms)
AssertionError [ERR_ASSERTION]: expected a refusal, got 4: usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS
4 !== 1
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:292:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/rockerduck-row334/packages/cli/tests/case2-unit.test.mjs:1106:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
+3
View File
@@ -1694,3 +1694,6 @@ are never rewritten or removed; corrections are new entries.
2026-10-24 | magica | row329 R2 #1827 | ASCII management-key fix and docs verified: package66/66,bus readers261/261,CLI readers726/726,config55/55; no required failures/skips, slot returned. Preparing authorized ff review candidate; old tests unchanged.
2026-10-24 | Sage 1ef1e4f8 | row329 landing #1827 (mos-1634) | Gate GREEN 23:29:14Z unchanged, landed 61ebf2c34 pushed 23:31:27Z; #1827 closed c31650; queue note 4063, done 4064 (b24d917ec); DEFERRED N2/R2-N1 to 331, N1 to P7; Vikunja 631 filed, 237 c1109; 284 and 157 must rebase.
2026-10-25 | Sage 1ef1e4f8 | row284 landing #1789 | Gate GREEN 00:09:04Z unchanged, landed ce0b6ed41 pushed 00:09:52Z; #1789 closed c31655; queue note 4071, done 4072 (7c8dc2033); DEFERRED Webby N1 to the next library read-path row; row 332 PROXY_CODEX_API ruled openai-responses (sage-4331), P3 holds for row 331.
2026-10-25 — rockerduck — row334 (#1832), Case 2 proxy system unit — design/read phase; tests held for Sage CLEAR; queue start lock diagnosed.
2026-10-25 — rockerduck — row334 (#1832), Case 2 proxy system unit — queue start rev4079; tests then implementation written, design accepted by Sage; verification held for CLEAR, P2 refusal-code check pending row331.
2026-10-25 — rockerduck — row334 RED-only slot sage-4387 — both whole files against pre-change scripts: 74 older tests pass, 34 new expected failures; implementations restored, slot returned; GREEN/mutants held.
-31
View File
@@ -65,37 +65,6 @@ Tell Jason which folders you want to mark. He adds them to the `supporting` list
entry of your business file. He edits that entry by hand, and you can ask him to add or change a folder
later.
## Making a crossover (hybrid) universe
A crossover is its own universe, called a hybrid. It reads the canon of the universes it draws on, its
sources, and has its own books. Agents only cross worlds inside one. Like the rest of the `projects`
entry, Jason adds it by hand and the agents can't change it. Tell him what you want, and he writes an
entry with these keys:
- `kind`: `"hybrid"`.
- `name`: the name you call it by.
- `sources`: two or more universes it reads.
- `books`: its own book folders. It may start with none.
- `playground` and `supporting`: its own folders, the same as for any universe.
- `constraints`: the rules below, if you want any.
- `notes`: text on how the worlds fit together, up to 4000 characters. The continuity agent reads the
notes and cites them.
A hybrid never reads a source's playground.
There are two kinds of constraint:
- `exclude` hides part of a source. It takes two forms. A source and a type folder, for example
`second/Characters`, hides that type folder in each of that source's books. A path, for example
`Book One/Research`, hides that folder or note, which must sit in a book of one of the sources. Agents
in the hybrid don't see what is hidden, can't cite it beside a hybrid note, and a hybrid note that links
to it gets a link warning.
- `pin` holds one fact fixed: a source note (`entity`, a `.md` path), a property (`field`) and its value
(`value`). If the note goes missing or the property changes, Mosaic warns. It also warns when a hybrid
note of the same type and name sets that property to something else. Linking to the note doesn't count:
a hybrid character who links `[[Mara]]` is someone else.
Mosaic refuses an entry whose `exclude` or `pin` points outside the sources' books, or into a source's
playground.
## Saves and agent notes
When you save a file in the Console, Mosaic makes a commit under the name and email you give Jason for
+1 -1
View File
@@ -95,7 +95,7 @@ With `--mapping` three lines follow:
|---|---|
| `scopes canon .., playground .., unmapped ..; indexed ..` | tree entries per scope, and notes indexed |
| `resolved root .., vault-prefix .., relative .., shortest .., unresolved ..` | links per resolver step. `unresolved` is the R4 count |
| `problems R1 .., ..., R8 ..` | problems per rule |
| `problems R1 .., ..., R7 ..` | problems per rule |
Folder paths and short property names are the only words from her repo in the output. In a folder
name, tab, newline, carriage return, backspace and form feed print as `\t`, `\n`, `\r`, `\b` and `\f`.
+1 -26
View File
@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { connect } from 'node:net';
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { Client } from '../src/client.mjs';
import { serve } from '../src/server.mjs';
@@ -68,31 +68,6 @@ test('C2-06 supporting file stores supporting scope and its real agent trailer',
assert.deepEqual(JSON.parse(event.body).check, { citations: [{ scope: 'supporting', agent: 'acme/scribe', file }] });
});
// C2-15: a hybrid over harbor and second that excludes harbor's Places folder (path form).
const crossMapping = entry => {
entry.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude: 'Book One/Places' }] };
return entry;
};
test('C2-15 excluded cite beside a hybrid refuses and inserts nothing', async t => {
const f = citationWorld(t, { mapChange: crossMapping }); f.call(f.agent, 'role.claim');
mkdirSync(join(f.repo, 'Crossover/Chapters'), { recursive: true });
writeFileSync(join(f.repo, 'Crossover/Chapters/01.md'), 'A hybrid fixture.\n');
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'hybrid fixture');
const commit = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(commit);
const hybrid = fixtureCite(commit, { path: 'Crossover/Chapters/01.md', lines: [1, 1], quote: 'hybrid fixture' });
const hidden = fixtureCite(commit, { path: 'Book One/Places/Harbor Town.md', lines: [4, 4], quote: 'Salt.' });
const count = f.store.get('SELECT count(*) n FROM messages').n;
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: citedBody([hybrid, hidden]) }),
e => e.code === 'citation-cross-universe' && e.reason === 'citation 2 (Book One/Places/Harbor Town.md) is excluded from hybrid cross');
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count);
assert.equal(f.store.all("SELECT * FROM events WHERE json_extract(body,'$.action')='message.checked'").length, 0);
// The hybrid beside source canon it can see goes through.
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([hybrid, fixtureCite(commit)]) });
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count + 1);
});
test('C2-06 check evidence and insert are atomic when storing evidence fails', async t => {
const f = citationWorld(t); f.call(f.agent, 'role.claim');
f.store.run(`CREATE TRIGGER fixture_check_refused BEFORE INSERT ON events
@@ -251,18 +251,3 @@ test('C2-05c broker forwards supporting scope and returns the served blob withou
assert.equal(await f.call('library.get', { universe: 'second', path, scope: 'supporting' }), null);
await assert.rejects(f.call('library.manuscript', { universe: 'harbor', book: 'Book One', scope: 'supporting' }), { code: 'query-argument-invalid' });
});
test('C2-15 broker: library.find and library.universe in a hybrid see its own files and source canon, minus the type-form exclude', async t => {
const projects = { books: mappingEntry() };
projects.books.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude: 'second/Characters' }], notes: 'Fixture crossover.' };
const f = await world(t, { projects, files: { ...TEXTS, 'Crossover/Chapters/01.md': '# Both\n[[Tomas]] and [[Elsewhere]].\n' } });
const found = await f.call('library.find', { universe: 'cross', query: '' });
assert.deepEqual(found.results.map(x => [x.path, x.universe]), [
['Book One/Chapters/01.md', 'harbor'], ['Book One/Characters/Tomas.md', 'harbor'], ['Crossover/Chapters/01.md', 'cross']]);
assert.deepEqual((await f.call('library.find', { universe: 'cross', query: '', scope: 'playground' })).results, []);
assert.ok(!(await f.call('library.find', { universe: 'harbor', query: '' })).results.some(x => x.path.startsWith('Crossover/')));
const universe = await f.call('library.universe', { universe: 'cross' });
assert.deepEqual([universe.kind, universe.sources, universe.constraints, universe.notes],
['hybrid', ['harbor', 'second'], [{ exclude: 'second/Characters' }], 'Fixture crossover.']);
});
@@ -67,12 +67,3 @@ test('C2-04 M9: a serialization hook cannot change projects after the canonical
Object.defineProperty(next.projects, 'toJSON', { value: () => changed });
});
});
test('C2-15: an organization write that adds a hybrid universe with exclude and pin constraints is protected', t => {
const f = fixture(t);
protectedWrite(f, next => {
next.projects.books.universes.cross = { kind: 'hybrid', name: 'An invented crossover', sources: ['harbor', 'second'],
books: ['Crossover'], playground: [], notes: 'Fixture only',
constraints: [{ exclude: 'second/Characters' }, { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } }] };
});
});
@@ -490,3 +490,10 @@ test('C2-21: library install supplies chief identity and books publish for its r
assert.ok(doc.roles.chief.identity?.gitea?.user);
assert.notEqual(doc.roles.chief.identity?.gitea?.user, 'writer');
});
test('row334 install Next tells root to select proxy only when configured', t => {
const x = world(t), r = install(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /Next: root runs case2-unit.sh[^\n]+add --proxy when the config has a proxy section/);
});
+170 -2
View File
@@ -7,10 +7,10 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { businessDoc, systemConfig, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -940,3 +940,171 @@ test("Vikunja 614 precheck: notifier config refuses missing, public, malformed a
assert.match(r.out, /REFUSED notifier config:/, `${fault}: ${r.out}`);
}
});
// Row334: render only into the scratch hub; fake systemctl never touches the host.
const PROXY_TEMPLATE = join(REPO, 'packages/cli/systemd/mosaic-proxy.service.in');
const proxyFile = h => join(h.units, 'mosaic-proxy.service');
const proxyUnit = (h, options = {}) => unit(h, { ...options, extra: ['--proxy', ...(options.extra ?? [])] });
test('row334 no-proxy renders stay byte-identical to the pre-row renderer', t => {
const h = hub(t);
// Immutable committed baseline, executed only with the same scratch seams.
const baseline = spawnSync('git', ['show', '78b69e1e6:scripts/case2-unit.sh'], { cwd: REPO, encoding: 'utf8' });
assert.equal(baseline.status, 0, baseline.stderr);
const old = script(join(h.root, 'baseline/scripts/case2-unit.sh'), baseline.stdout);
const args = ['--user', 'mosaic', '--home', h.home, '--business', 'acme', '--copy', COPY_A, '--node', USR_NODE, '--', ...libraryFlags(h)];
const r = spawnSync(old, args, { encoding: 'utf8', env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units,
CASE2_SYSTEMCTL: join(h.bin, 'systemctl'), CASE2_GETENT: join(h.bin, 'getent'), CASE2_BUS_TEMPLATE: BUS_TEMPLATE, CASE2_WEBUI_TEMPLATE: WEBUI_TEMPLATE } });
assert.equal(r.status, 0, r.stderr);
const before = [read(busFile(h)), read(webuiFile(h))];
assert.equal(unit(h).code, 0);
assert.deepEqual([read(busFile(h)), read(webuiFile(h))], before);
assert.equal(existsSync(proxyFile(h)), false);
});
test('row334 proxy uses P1 runner, bus dependencies and proxy bus Console start order', t => {
const h = hub(t);
const r = proxyUnit(h, { profile: 'factory', flags: ['--port', '443', '--business', 'acme'], extra: ['--claude', TRUE_BIN] });
assert.equal(r.code, 0, r.err);
const proxy = read(proxyFile(h));
const expected = read(PROXY_TEMPLATE).replaceAll('@REPO@', copyDir(h)).replaceAll('@PATH@', '/usr/bin:/usr/local/bin:/bin')
.replaceAll('@NODE@', USR_NODE).replace(/^@CONFIG_ENV@\n/m, '').replace('WantedBy=default.target', 'WantedBy=multi-user.target')
.replace('TimeoutStopSec=60', `TimeoutStopSec=60\nUser=mosaic\nGroup=mosaic\n${HARDENING(h).join('\n')}`);
assert.equal(proxy, expected);
assert.equal(statSync(proxyFile(h)).mode & 0o777, 0o644);
assert.match(read(busFile(h)), /Description=[^\n]+\nWants=mosaic-proxy.service\nAfter=mosaic-proxy.service\n/);
assert.deepEqual(lines(proxy, 'ExecStart'), [`${USR_NODE} ${copyDir(h)}/packages/model-proxy/src/run.mjs`]);
assert.doesNotMatch(proxy, /EnvironmentFile=|Capabilities=|CapabilityBoundingSet=|MOSAIC_CLAUDE_BIN/);
assert.doesNotMatch(read(webuiFile(h)), /Wants=|After=/);
assert.deepEqual(read(h.log).trim().split('\n'), [
'is-active --quiet mosaic-proxy.service', 'is-active --quiet [email protected]', 'is-active --quiet mosaic-webui.service',
'daemon-reload', 'enable mosaic-proxy.service [email protected] mosaic-webui.service',
'start mosaic-proxy.service', 'start [email protected]', 'start mosaic-webui.service',
]);
});
const proxyRefusals = [
['continuation', s => s.replace('Type=simple', 'Type=simple' + String.fromCharCode(92)), /a line continues onto the next/],
['not-setting', s => s.replace('Type=simple', 'not a setting'), /neither a section/],
...['RestrictNamespaces', 'PrivateUsers', 'SystemCallFilter', 'PermissionsStartOnly', 'EnvironmentFile', 'AmbientCapabilities', 'CapabilityBoundingSet'].map(k =>
[k, s => s.replace('Type=simple', `Type=simple\n${k}=yes`), new RegExp(`refusing ${k}=`)]),
['privileged', s => s.replace('ExecStart=', 'ExecStart=+'), /Exec line that runs with full privileges/],
['direct-binary', s => s.replace('packages/model-proxy/src/run.mjs', 'opt/cli-proxy-api'), /ExecStart= must be exactly once/],
['nnp', s => s.replace('NoNewPrivileges=yes', 'NoNewPrivileges=no'), /NoNewPrivileges= must be exactly once/],
['cwd', s => s.replace('WorkingDirectory=@REPO@', 'WorkingDirectory=/elsewhere'), /WorkingDirectory= must be exactly once/],
['environment', s => s.replace('Environment=PATH=@PATH@', 'Environment=PATH=/elsewhere'), /Environment= lines must be exactly/],
['install', s => s.replace('WantedBy=default.target', 'WantedBy=other.target'), /WantedBy= must be exactly once/],
['user', s => s.replace('Type=simple', 'Type=simple\nUser=other'), /User= must be exactly once/],
['group', s => s.replace('Type=simple', 'Type=simple\nGroup=other'), /Group= must be exactly once/],
...['ProtectSystem', 'ReadWritePaths', 'ProtectHome', 'PrivateTmp'].map(k =>
[k, s => s.replace('Type=simple', `Type=simple\n${k}=other`), new RegExp(`${k}= must be exactly once`)]),
['token', s => s.replace('Type=simple', 'Type=simple\nX-Note=@UNKNOWN@'), /a token is left after the render/],
];
for (const [name, edit, message] of proxyRefusals) test(`row334 proxy refuses ${name} by message before writing`, t => {
const h = hub(t);
const file = template(h, PROXY_TEMPLATE, `${name}.in`, edit);
refuses(proxyUnit(h, { env: { CASE2_PROXY_TEMPLATE: file } }), message);
assert.deepEqual(readdirSync(h.units), []);
assert.equal(existsSync(h.log), false);
});
test('row334 proxy template missing or symlink refuses', t => {
const h = hub(t), missing = join(h.root, 'missing.in');
refuses(proxyUnit(h, { env: { CASE2_PROXY_TEMPLATE: missing } }), /template missing or not a regular file/);
symlinkSync(PROXY_TEMPLATE, missing);
refuses(proxyUnit(h, { env: { CASE2_PROXY_TEMPLATE: missing } }), /template missing or not a regular file/);
});
test('row334 rendered ExecStart refuses wrong PIN and missing proxy with P1 status', t => {
const h = hub(t);
assert.equal(proxyUnit(h).code, 0);
const copy = copyDir(h);
cpSync(join(REPO, 'packages/model-proxy'), join(copy, 'packages/model-proxy'), { recursive: true });
cpSync(join(REPO, 'scripts/mosaic-config.mjs'), join(copy, 'scripts/mosaic-config.mjs'));
const config = systemConfig(join(h.root, 'runner'));
const doc = JSON.parse(read(config));
mkdirSync(doc.dataRoot, { recursive: true, mode: 0o700 });
const pin = JSON.parse(read(join(REPO, 'packages/model-proxy/PIN')));
const binary = join(doc.dataRoot, 'opt/cliproxyapi', pin.version, 'cli-proxy-api');
script(binary, `echo unexpected-spawn > '${h.root}/spawned'`);
writeJson(join(dirname(config), 'proxy.json'), { port: 18541, binary });
const argv = lines(read(proxyFile(h)), 'ExecStart')[0].split(' ');
const run = () => spawnSync(argv[0], argv.slice(1), { encoding: 'utf8', env: { PATH: '/usr/bin:/bin', HOME: h.home, MOSAIC_CONFIG: config } });
const bad = run();
assert.equal(bad.status, 3, bad.stderr);
assert.equal(bad.stderr, 'proxy-install-invalid\n');
assert.equal(existsSync(join(h.root, 'spawned')), false);
rmSync(join(dirname(config), 'proxy.json'));
const absent = run();
assert.equal(absent.status, 3, absent.stderr);
assert.equal(absent.stderr, 'proxy-not-configured\n');
});
test('row334 reinstall repins all three and refuses a proxy copy mismatch', t => {
const h = hub(t);
assert.equal(proxyUnit(h).code, 0);
assert.equal(proxyUnit(h, { copy: COPY_B }).code, 0);
for (const file of [proxyFile(h), busFile(h), webuiFile(h)]) assert.match(read(file), new RegExp(copyDir(h, COPY_B)));
writeFileSync(proxyFile(h), read(proxyFile(h)).replaceAll(copyDir(h, COPY_B), copyDir(h)));
const before = [proxyFile(h), busFile(h), webuiFile(h)].map(read);
refuses(proxyUnit(h), /installed units name different copies/);
assert.deepEqual([proxyFile(h), busFile(h), webuiFile(h)].map(read), before);
});
test('row334 installed proxy without --proxy refuses and preserves all bytes', t => {
const h = hub(t);
assert.equal(proxyUnit(h).code, 0);
const files = [proxyFile(h), busFile(h), webuiFile(h)], before = files.map(read);
writeFileSync(h.log, '');
refuses(unit(h), /installed mosaic-proxy.service requires --proxy/);
assert.deepEqual(files.map(read), before);
assert.equal(read(h.log), '');
});
test('row334 dangling proxy symlink without flag refuses without deleting', t => {
const h = hub(t);
symlinkSync(join(h.root, 'absent'), proxyFile(h));
refuses(unit(h), /installed mosaic-proxy.service requires --proxy/);
assert.equal(readdirSync(h.units).includes('mosaic-proxy.service'), true);
});
test('row334 active proxy reports old copy and all three must restart together', t => {
const h = hub(t);
assert.equal(proxyUnit(h).code, 0);
const r = proxyUnit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: 'mosaic-proxy.service [email protected] mosaic-webui.service' } });
assert.equal(r.code, 0, r.err);
assert.match(r.out, /mosaic-proxy.service was already running/);
assert.match(r.out, /restart mosaic-proxy.service, [email protected] and mosaic-webui.service together/);
});
for (const name of ['mosaic-proxy.service', '[email protected]']) test(`row334 failed ${name} start leaves later units unstarted`, t => {
const h = hub(t);
refuses(proxyUnit(h, { env: { CASE2_TEST_FAIL: `start ${name}` } }), /systemctl start .* failed/);
assert.doesNotMatch(read(h.log), /start mosaic-webui.service/);
if (name === 'mosaic-proxy.service') assert.doesNotMatch(read(h.log), /start [email protected]/);
});
test('row334 installed proxy enforces hardening order and remaining common refusals', t => {
const h = hub(t);
assert.equal(proxyUnit(h).code, 0);
const original = read(proxyFile(h));
for (const [edit, message] of [
[s => s.replace('ProtectSystem=strict\nReadWritePaths=' + h.home, 'ReadWritePaths=' + h.home + '\nProtectSystem=strict'), /hardening lines must be in the order/],
[s => s.replace('WorkingDirectory=' + copyDir(h), 'WorkingDirectory=%h'), /names no copy/],
[s => s.replace('Type=simple', 'Type=simple\nX-Note=%h'), /%h is left; a system unit/],
[s => s.replace('RestartSec=15', 'RestartSec=16'), /differs from the render in more than the copy id/],
]) {
writeFileSync(proxyFile(h), edit(original));
refuses(proxyUnit(h), message);
}
});
test('row334 proxy installed path rejects symlink and non-regular file', t => {
const h = hub(t);
symlinkSync(join(h.root, 'absent'), proxyFile(h));
refuses(proxyUnit(h), /is a symlink/);
rmSync(proxyFile(h));
mkdirSync(proxyFile(h));
refuses(proxyUnit(h), /is not a regular file/);
});
+1 -2
View File
@@ -119,9 +119,8 @@ Problems carry `{ code, path, line, message }`:
| R3 | front matter not fully parsed, `line` is where parsing stopped | yes, front matter kept raw |
| R4 | link resolves to no file or to more than one | yes, link dropped |
| R5 | canon file links into the playground | yes |
| R6 | canon file links into another universe, a hybrid's sources excepted, or a hybrid's file links into a path its `exclude` covers | yes |
| R6 | canon file links into another universe, a hybrid's sources excepted | yes |
| R7 | two paths differ only in case | no, neither |
| R8 | pinned value changed: a hybrid's pinned entity is missing or its `field` differs from `value`, or a hybrid version of it sets `field` to another value | yes |
The front matter parser takes scalars, quoted strings, flow lists, block lists and wikilinks. Nested
maps, anchors, block scalars and duplicate keys stop it with R3.
+1 -6
View File
@@ -3,7 +3,7 @@
// only the citations, never what the text says. Codes are design 5.2's table.
import { lstatSync } from 'node:fs';
import { git } from '../../project/src/git.mjs';
import { classify, excluded } from './scope.mjs';
import { classify } from './scope.mjs';
import { listTree, readLibrary, readTexts, splitLines } from './reader.mjs';
import { LibraryError } from './problems.mjs';
import { mirrorPath, readSyncState } from './mirror.mjs';
@@ -94,11 +94,6 @@ export async function checkReply(body, { limits, mapping, library, web }) {
const sources = hybrid ? mapping.universes[hybrid].sources : [];
if (!hybrid || !universes.every(u => u === hybrid || sources.includes(u)))
refuse('citation-cross-universe', `the citations span universes ${universes.join(', ')}`);
// C2-15: beside a hybrid cite, a source path the hybrid excludes is outside the hybrid's world.
citations.forEach((c, i) => {
if (found[i] && found[i].universe !== hybrid && excluded(mapping, hybrid, c.path))
refuse('citation-cross-universe', `citation ${i + 1} (${c.path}) is excluded from hybrid ${hybrid}`);
});
}
citations.forEach((c, i) => {
if (!Object.hasOwn(c, 'url')) return;
+1 -1
View File
@@ -1,7 +1,7 @@
// packages/library: her writing repo read by a mapping (Case 2, design 1 and 7.4). See README.md.
export { LibraryError, RULES, problem, sortProblems } from './problems.mjs';
export { checkMapping, TYPES } from './mapping.mjs';
export { classify, excluded } from './scope.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';
-27
View File
@@ -5,7 +5,6 @@
// 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';
import { typeForm } from './scope.mjs';
export const TYPES = Object.freeze(['character', 'place', 'research', 'tracker', 'summary']);
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -145,32 +144,6 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
bad(`universes.${id}.sources names ${JSON.stringify(source)}, which is not a plain universe`);
}
}
// C2-15 (design 1.4): each constraint against the hybrid's sources, now that every source is read. An
// exclude reads as the type form `source/Type` or as a path in a source's book, never both. A path, or
// a pinned entity, under a source's playground is one the hybrid never reads (REQ-LIB-3). Whether the
// path exists at a commit is the reader's to check.
for (const [id, universe] of Object.entries(universes)) {
if (universe.kind !== 'hybrid') continue;
const sources = universe.sources.map(source => [source, universes[source]]);
const inSourceBook = path => sources.some(([, s]) => s.books.some(book => path === book || holds(book, path)));
const underPlayground = path => sources.find(([, s]) => s.playground.some(p => path === p.slice(0, -1) || path.startsWith(p)))?.[0];
const sourcePath = (path, at) => {
if (!inSourceBook(path)) bad(`${at} ${JSON.stringify(path)} lies inside no book of the hybrid's sources`);
const owner = underPlayground(path);
if (owner) bad(`${at} ${JSON.stringify(path)} lies under a playground of universes.${owner}, which the hybrid never reads`);
};
universe.constraints.forEach((constraint, i) => {
const at = `universes.${id}.constraints[${i}]`;
if (constraint.pin) {
if (!constraint.pin.entity.endsWith('.md')) bad(`${at}.pin.entity ${JSON.stringify(constraint.pin.entity)} is not a .md file`);
return sourcePath(constraint.pin.entity, `${at}.pin.entity`);
}
const type = typeForm(entry, universe, constraint.exclude);
if (type && inSourceBook(constraint.exclude))
bad(`${at}.exclude ${JSON.stringify(constraint.exclude)} reads as both a source/type and a source path`);
if (!type) sourcePath(constraint.exclude, `${at}.exclude`);
});
}
const books = [...bookOwner.keys()];
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)}`);
-1
View File
@@ -13,7 +13,6 @@ export const RULES = Object.freeze({
R5: 'canon file refers into the playground',
R6: 'canon file refers into another universe',
R7: 'paths differ only in case',
R8: 'pinned value changed',
});
export function problem(code, path, line, message = RULES[code]) {
+4 -4
View File
@@ -8,7 +8,6 @@ import { raise } from './problems.mjs';
import { TYPES } from './mapping.mjs';
import { indexFor } from './index-build.mjs';
import { listTree } from './reader.mjs';
import { excluded } from './scope.mjs';
// The same cap as packages/harness/src/tools.mjs REPLY_CAP. The query tests assert the two stay equal.
export const QUERY_REPLY_CAP = 40_000;
@@ -20,6 +19,7 @@ const SCOPE_RANK = Object.freeze({ supporting: 0, playground: 1, canon: 2 });
const bytes = value => JSON.stringify(value).length;
const invalid = message => raise('query-argument-invalid', message);
const inside = (folder, path) => path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
function textArg(value, name) {
if (typeof value !== 'string') invalid(`${name} must be text`);
@@ -47,13 +47,13 @@ function universeArg(mapping, value) {
}
// Whether universe U may see a file. Its own files, in either scope. A hybrid also sees the canon of
// its declared sources, minus what its exclude constraints cover (scope.mjs excluded(), C2-15). A hybrid
// never sees a source's playground. Pin constraints hide nothing; the reader reports them as R8.
// its declared sources, minus the folders its exclude constraints name. A hybrid never sees a source's
// playground. Pin constraints do not hide anything; they are reported by universeInfo only.
export function sees(mapping, universe, file) {
if (file.universe === universe) return true;
const entry = mapping.universes[universe];
if (entry.kind !== 'hybrid' || !entry.sources.includes(file.universe) || file.scope !== 'canon') return false;
return !excluded(mapping, universe, file.path);
return !entry.constraints.some(c => c.exclude !== undefined && inside(c.exclude, file.path));
}
// The files one universe may see in one scope, with optional book and type filters, in path order.
+3 -50
View File
@@ -4,7 +4,7 @@
// way. Nothing in the repo is checked out or run (D7). Each R-rule is a problem, never a refusal
// of the commit; the affected file or link is left out of the index.
import { git, gitBytes } from '../../project/src/git.mjs';
import { classify, excluded } from './scope.mjs';
import { classify } from './scope.mjs';
import { extractLinks, isAttachment, parseWikilink, resolver } from './links.mjs';
import { LibraryError, problem, raise, sortProblems } from './problems.mjs';
@@ -169,7 +169,7 @@ function frontMatterLine(lines, bodyLine, field) {
}
// Read a commit under a checked mapping (mapping.mjs). Returns the indexed files and every
// R1-R8 problem: { commit, files: [...], problems: [...] }, frozen, sorted by path.
// R1-R7 problem: { commit, files: [...], problems: [...] }, frozen, sorted by path.
export async function readLibrary({ repo, commit, mapping }) {
if (!mapping || mapping.kind !== 'library') throw new LibraryError('library-mapping-invalid', 'expected a checked library mapping');
const entries = await listTree(repo, commit);
@@ -206,64 +206,17 @@ export async function readLibrary({ repo, commit, mapping }) {
if (file.scope === 'canon') {
if (to.scope === 'playground') problems.push(problem('R5', file.path, link.line));
const sources = mapping.universes[file.universe].sources ?? [];
// A hybrid's link into a path its exclude covers is R6 too (C2-15). An excluded path is always in a
// source, so a link gets at most one R6.
if ((to.universe !== file.universe && !sources.includes(to.universe)) || excluded(mapping, file.universe, to.path))
problems.push(problem('R6', file.path, link.line));
if (to.universe !== file.universe && !sources.includes(to.universe)) problems.push(problem('R6', file.path, link.line));
}
kept.push({ ...link, path: target.path, step: target.step });
}
file.links = kept;
delete file.stop;
}
problems.push(...pinProblems(mapping, files, byPath));
files.sort((a, b) => a.path < b.path ? -1 : 1);
return deepFreeze({ commit, files, problems: sortProblems(problems) });
}
// R8 (C2-15, design 1.4): each pin of each hybrid. The pinned entity gets R8 when it isn't indexed (absent,
// R1, R2 or R7) or its front matter doesn't hold `field` as a scalar whose text equals String(value); a
// list, null, a missing key and R3 front matter all count as changed. A hybrid canon file is a version of
// the entity when classify gives it the entity's type and its name matches: the trimmed front matter
// `name` on both sides, or the file names when either has no text name. A version that sets `field` to
// another value gets R8 on that key's line. A link alone never makes a version.
function pinProblems(mapping, files, byPath) {
const out = [];
for (const [hybrid, entry] of Object.entries(mapping.universes)) {
if (entry.kind !== 'hybrid') continue;
for (const { pin } of entry.constraints.filter(c => c.pin)) {
const entity = byPath.get(pin.entity);
if (!entity) out.push(problem('R8', pin.entity));
else if (!pinned(entity, pin)) out.push(problem('R8', pin.entity, keyLine(entity, pin.field)));
const type = classify(mapping, pin.entity).type;
const stem = pin.entity.split('/').pop().replace(/\.md$/, '');
const name = entity ? textName(entity) : null;
for (const file of files) {
if (file.universe !== hybrid || file.scope !== 'canon' || file.type !== type) continue;
const own = textName(file);
if ((name !== null && own !== null ? own !== name : file.name !== stem)) continue;
if (file.frontmatter && Object.hasOwn(file.frontmatter, pin.field) && !pinned(file, pin))
out.push(problem('R8', file.path, keyLine(file, pin.field)));
}
}
}
// Two pins (or two hybrids) on one missing or changed entity give one entry per path and line (Filbert N1).
const seen = new Set();
return out.filter(p => { const key = `${p.path}\0${p.line ?? ''}`; return !seen.has(key) && seen.add(key); });
}
const textName = file => typeof file.frontmatter?.name === 'string' && file.frontmatter.name.trim() ? file.frontmatter.name.trim() : null;
function pinned(file, { field, value }) {
const held = file.frontmatter && Object.hasOwn(file.frontmatter, field) ? file.frontmatter[field] : undefined;
return ['string', 'number', 'boolean'].includes(typeof held) && String(held) === String(value);
}
// The front matter line of `field` (bare or quoted key), or undefined.
function keyLine(file, field) {
if (file.bodyLine < 3) return undefined;
const key = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const at = file.lines.slice(1, file.bodyLine - 2).find(([, t]) => new RegExp(`^["']?${key}["']?\\s*:`).test(t));
return at?.[0];
}
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.freeze(value); for (const v of Object.values(value)) deepFreeze(v); }
return value;
-23
View File
@@ -36,26 +36,3 @@ function place(mapping, path, under, where) {
: mapping.manuscript.includes(folder) ? 'manuscript' : null;
return { ...where, type, folder };
}
// C2-15 (design 1.4): whether an `exclude` constraint of universe `hybrid` covers `path`. The type form
// `source/Type` (first segment one of the hybrid's sources, the rest one key of `types`) covers that type
// folder in each of the source's books. Otherwise the exclude is a path and covers itself and what lies
// inside it. checkMapping keeps the two readings from both fitting. False for a universe that isn't a
// hybrid. Reads only the path (D2).
export function excluded(mapping, hybrid, path) {
const entry = mapping.universes[hybrid];
if (entry?.kind !== 'hybrid' || typeof path !== 'string') return false;
return entry.constraints.some(c => {
if (c.exclude === undefined) return false;
const type = typeForm(mapping, entry, c.exclude);
if (type) return mapping.universes[type.source].books.some(book => path.startsWith(`${book}/${type.folder}/`));
return path === c.exclude || path.startsWith(`${c.exclude}/`);
});
}
// { source, folder } when `exclude` reads as the type form for this hybrid entry, else null.
export function typeForm(mapping, entry, exclude) {
const [source, ...rest] = exclude.split('/');
const folder = rest.join('/');
return entry.sources.includes(source) && rest.length === 1 && Object.hasOwn(mapping.types, folder) ? { source, folder } : null;
}
-19
View File
@@ -73,25 +73,6 @@ test('C2-06 independent universes refuse; a hybrid and only its sources pass', a
assert.equal((await checkReply(body([hybrid, cite(r.commit), other]), c)).citations.length, 3);
});
test('C2-15 a hybrid cite beside an excluded source path refuses citation-cross-universe, both forms', async t => {
const r = repo(t, { ...BASE, 'Crossover/Chapters/01.md': 'A hybrid fixture.\n', 'Second World/Characters/Ilse.md': 'Ilse fixture.\n',
'Book One/Research/Logs.md': 'Logs fixture.\n' }), c = context(r);
const at = (path, quote) => cite(r.commit, { path, lines: [1, 1], quote });
const hybrid = at('Crossover/Chapters/01.md', 'hybrid fixture');
for (const [exclude, hidden, quote] of [['second/Characters', 'Second World/Characters/Ilse.md', 'Ilse fixture'],
['Book One/Research', 'Book One/Research/Logs.md', 'Logs fixture']]) {
c.mapping = mapping(e => { e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude }] }; return e; });
c.library = libraryReader(r.repo, r.commit, c.mapping);
await assert.rejects(checkReply(body([hybrid, cite(r.commit), at(hidden, quote)]), c),
e => refusal('citation-cross-universe')(e) && e.reason === `citation 3 (${hidden}) is excluded from hybrid cross`, exclude);
// The hybrid with source canon it can see passes.
assert.equal((await checkReply(body([hybrid, cite(r.commit)]), c)).citations.length, 2, exclude);
// A cite of only the excluded source path is that source universe's item, as before.
assert.deepEqual((await checkReply(body([at(hidden, quote)]), c)).citations.map(x => x.scope), ['canon'], exclude);
}
});
test('C2-06 web cites require allowed definition, this session fetch and kept quote', async t => {
const c = context(repo(t)), web = { url: 'https://fixture.invalid/page', quote: 'kept text' };
await assert.rejects(checkReply(body([web]), c), refusal('citation-unfetched'));
-97
View File
@@ -1,97 +0,0 @@
// C2-15 (row 267): a hybrid universe end to end. A business-file-shaped mapping (the fixture mapping plus a
// `cross` hybrid over harbor and second) on a repo in her layout's shape, read through the C2-05a queries,
// once for each exclude form of design 1.4. Bodies are made up; the repos are built under the scratch area.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { excluded } from '../src/scope.mjs';
import { find, get, search, universeInfo } from '../src/query.mjs';
import { mapping, MARKER } from './helpers.mjs';
import { scratchRepo, note } from './query-fixture.mjs';
const FILES = {
'Book One/Characters/Mara Vell.md': note('character', 'Mara Vell', `Grey eyes. ${MARKER}`),
'Book One/Research/Ferry Logs.md': note('research', 'Ferry Logs', `Timetable. ${MARKER}`),
'Book One/Playground/What If.md': note('summary', 'What If', `${MARKER} [[Mara Vell]]`),
'Second World/Characters/Ilse.md': note('character', 'Ilse', `${MARKER} Ilse.`),
'Second World/Places/Gate.md': note('place', 'Gate', `${MARKER} Gate.`),
'Crossover/Chapters/01.md': `# Meeting\n${MARKER} [[Mara Vell]] meets [[Ilse]] at [[Gate]].\nShe reads [[Ferry Logs]].\n`,
'Crossover/Places/Bridge.md': note('place', 'Bridge', `${MARKER} Bridge.`),
};
// The two forms of one rule: the type form hides second's Characters folder; the path form hides harbor's
// Research folder.
const FORMS = {
type: { exclude: 'second/Characters', hidden: 'Second World/Characters/Ilse.md' },
path: { exclude: 'Book One/Research', hidden: 'Book One/Research/Ferry Logs.md' },
};
const NOTES = 'Ilse crosses over after book one.';
const withCross = exclude => mapping(e => {
e.universes.cross = { kind: 'hybrid', name: 'Crossover', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude }, { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } }],
notes: NOTES };
return e;
});
const src = (ref, map) => ({ repo: ref.repo, commit: ref.commit, mapping: map });
const ALL_CANON = ['Book One/Characters/Mara Vell.md', 'Book One/Research/Ferry Logs.md', 'Crossover/Chapters/01.md',
'Crossover/Places/Bridge.md', 'Second World/Characters/Ilse.md', 'Second World/Places/Gate.md'];
test('C2-15 excluded(): the type form covers the type folder in each source book, the path form its own subtree', () => {
const type = withCross('second/Characters'), path = withCross('Book One/Research');
assert.equal(excluded(type, 'cross', 'Second World/Characters/Ilse.md'), true);
assert.equal(excluded(type, 'cross', 'Second World/Characters/Deep/Ilse.md'), true);
assert.equal(excluded(type, 'cross', 'Second World/Places/Gate.md'), false);
assert.equal(excluded(type, 'cross', 'Book One/Characters/Mara Vell.md'), false);
assert.equal(excluded(type, 'cross', 'second/Characters/Ilse.md'), false);
assert.equal(excluded(path, 'cross', 'Book One/Research/Ferry Logs.md'), true);
assert.equal(excluded(path, 'cross', 'Book One/Research'), true);
assert.equal(excluded(path, 'cross', 'Book One/Research Notes/Other.md'), false);
assert.equal(excluded(path, 'cross', 'Second World/Characters/Ilse.md'), false);
// Not a hybrid, or no such universe: nothing is excluded.
assert.equal(excluded(path, 'harbor', 'Book One/Research/Ferry Logs.md'), false);
assert.equal(excluded(path, 'nowhere', 'Book One/Research/Ferry Logs.md'), false);
});
for (const [form, { exclude, hidden }] of Object.entries(FORMS)) {
test(`C2-15 ${form} form: find and search in the hybrid return its own files and the sources' canon, never a source playground or the excluded path`, async t => {
const s = src(scratchRepo(t, FILES), withCross(exclude));
const found = (await find(s, { universe: 'cross', query: '' })).results;
assert.deepEqual(found.map(r => r.path), ALL_CANON.filter(p => p !== hidden));
for (const r of found) assert.equal(r.universe, r.path.startsWith('Crossover/') ? 'cross' : r.path.startsWith('Book One/') ? 'harbor' : 'second');
assert.deepEqual((await find(s, { universe: 'cross', query: '', scope: 'playground' })).results, []);
const hits = (await search(s, { universe: 'cross', text: MARKER })).results.map(r => r.path);
assert.ok(!hits.includes(hidden) && !hits.includes('Book One/Playground/What If.md'));
assert.ok(hits.includes('Crossover/Places/Bridge.md'));
assert.ok(hits.includes(form === 'type' ? 'Book One/Research/Ferry Logs.md' : 'Second World/Characters/Ilse.md'));
assert.equal(await get(s, { universe: 'cross', path: hidden }), null);
});
test(`C2-15 ${form} form: get keeps the hybrid's links into its sources and drops the excluded target`, async t => {
const s = src(scratchRepo(t, FILES), withCross(exclude));
const chapter = await get(s, { universe: 'cross', path: 'Crossover/Chapters/01.md' });
const out = chapter.links_out.map(l => l.path);
assert.ok(out.includes('Book One/Characters/Mara Vell.md'));
assert.ok(out.includes('Second World/Places/Gate.md'));
assert.ok(!out.includes(hidden));
});
}
test('C2-15 a source universe never sees a hybrid file', async t => {
const s = src(scratchRepo(t, FILES), withCross('Book One/Research'));
for (const universe of ['harbor', 'second']) {
assert.ok(!(await find(s, { universe, query: '' })).results.some(r => r.path.startsWith('Crossover/')), universe);
assert.ok(!(await search(s, { universe, text: MARKER })).results.some(r => r.path.startsWith('Crossover/')), universe);
assert.equal(await get(s, { universe, path: 'Crossover/Chapters/01.md' }), null);
}
const mara = await get(s, { universe: 'harbor', path: 'Book One/Characters/Mara Vell.md' });
assert.ok(!mara.links_in.some(l => l.path.startsWith('Crossover/')));
});
test('C2-15 universeInfo returns the hybrid\'s sources, constraints and notes', async t => {
const s = src(scratchRepo(t, FILES), withCross('second/Characters'));
const info = await universeInfo(s, { universe: 'cross' });
assert.equal(info.kind, 'hybrid');
assert.deepEqual(info.sources, ['harbor', 'second']);
assert.deepEqual(info.constraints, [{ exclude: 'second/Characters' },
{ pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } }]);
assert.equal(info.notes, NOTES);
});
-28
View File
@@ -212,34 +212,6 @@ test('a hybrid universe is a shape the query code can take (1.4), with sources t
refuses(e => { hybrid(e); e.universes.cross.constraints = [{ exclude: 'x', pin: {} }]; return e; }, 'library-mapping-invalid', /exactly one/);
});
test('C2-15: each exclude and pin is checked against the hybrid\'s sources', () => {
const cross = (...constraints) => e => {
e.universes.cross = { kind: 'hybrid', name: 'Crossover', sources: ['harbor', 'second'], books: ['Crossover'], constraints };
return e;
};
const pin = entity => ({ pin: { entity, field: 'eyes', value: 'grey' } });
// Both forms, a whole source book and a pin in a source book check.
assert.equal(mapping(cross({ exclude: 'second/Characters' }, { exclude: 'Book One/Research' }, { exclude: 'Second World' },
pin('Second World/Characters/Ilse.md'))).universes.cross.constraints.length, 4);
// Both readings fit: second's book folder is also named "second".
refuses(e => { cross({ exclude: 'second/Characters' })(e); e.universes.second.books = ['second']; return e; },
'library-mapping-invalid', /constraints\[0\]\.exclude "second\/Characters" reads as both a source\/type and a source path/);
// Neither fits: an unmapped path, the hybrid's own book, a type that isn't a key, a non-source universe.
for (const exclude of ['Elsewhere/Characters', 'Crossover/Characters', 'second/Ghosts', 'harbor', 'Book'])
refuses(cross({ exclude }), 'library-mapping-invalid', /\.exclude ".*" lies inside no book of the hybrid's sources/);
refuses(e => { cross({ exclude: 'third/Characters' })(e); e.universes.third = { name: 'Third', books: ['Third World'] }; return e; },
'library-mapping-invalid', /lies inside no book of the hybrid's sources/);
// Under a source's playground, as the prefix itself or inside it.
for (const exclude of ['Book One/Playground', 'Book One/Playground/Ideas'])
refuses(cross({ exclude }), 'library-mapping-invalid', /\.exclude ".*" lies under a playground of universes\.harbor, which the hybrid never reads/);
refuses(cross(pin('Book One/Playground/What If.md')), 'library-mapping-invalid',
/\.pin\.entity "Book One\/Playground\/What If\.md" lies under a playground of universes\.harbor/);
refuses(cross(pin('Crossover/Characters/Mara Vell.md')), 'library-mapping-invalid', /\.pin\.entity .* lies inside no book of the hybrid's sources/);
// Q5: a pinned entity is one .md note.
for (const entity of ['Book One/Characters/Mara Vell', 'Book One/Characters', 'Book One/Board.canvas'])
refuses(cross(pin(entity)), 'library-mapping-invalid', /\.pin\.entity ".*" is not a \.md file/);
});
test('a caller-supplied fail receives the code, as business file loading will pass it (C2-04)', () => {
const seen = [];
const fail = (code, message) => { seen.push(code); throw new Error(message); };
+1 -76
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { readLibrary, parseFrontMatter, LibraryError, MAX_BLOB } from '../src/index.mjs';
import { BASE, git, MARKER, mapping, repo } from './helpers.mjs';
import { BASE, git, mapping, repo } from './helpers.mjs';
const codes = (result, path) => result.problems.filter(p => p.path === path).map(p => p.code);
const file = (result, path) => result.files.find(f => f.path === path);
@@ -150,81 +150,6 @@ test('R6: a canon file that refers into another universe is reported; a hybrid m
assert.deepEqual(codes(result, 'Crossover/Chapters/A.md'), []);
});
// C2-15: a hybrid over harbor and second with one exclude and, optionally, a pin on Mara's eyes.
const crossWith = (...constraints) => mapping(e => {
e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'], constraints };
return e;
});
const EYES = { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'eyes', value: 'grey' } };
const character = (name, extra = []) => ['---', 'type: character', ...(name ? [`name: ${name}`] : []), ...extra, '---', MARKER, ''].join('\n');
const MARA_GREY = character('Mara Vell', ['eyes: grey']);
test('C2-15 R6: a hybrid file that links into an excluded source path is reported, in both exclude forms', async t => {
const r = repo(t, { ...BASE, 'Second World/Characters/Ilse.md': character('Ilse'), 'Book One/Research/Logs.md': '# Logs\n',
'Crossover/Chapters/A.md': 'From [[Mara Vell]] and [[Ilse]].\nThen [[Logs]].\n' });
for (const [exclude, line] of [['second/Characters', 1], ['Book One/Research', 2]]) {
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith({ exclude }) });
assert.deepEqual(result.problems.filter(p => p.path === 'Crossover/Chapters/A.md'),
[{ code: 'R6', path: 'Crossover/Chapters/A.md', line, message: 'canon file refers into another universe' }], exclude);
// The link stays in the file's links, as for every R-rule; the index's sees() is what hides it.
assert.equal(file(result, 'Crossover/Chapters/A.md').links.length, 3);
// A source's own link into the same path is not the hybrid's and stays clean.
assert.deepEqual(codes(result, 'Book One/Chapters/Chapter 01.md'), []);
}
});
test('C2-15 R8: the pinned entity reads clean when its field holds the value, and is reported when missing or changed', async t => {
const at = async files => {
const r = repo(t, { ...BASE, ...files });
return (await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8');
};
assert.deepEqual(await at({ 'Book One/Characters/Mara Vell.md': MARA_GREY }), []);
// Missing: no line.
const base = { ...BASE }; delete base['Book One/Characters/Mara Vell.md'];
const r = repo(t, base);
assert.deepEqual((await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8'),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', message: 'pinned value changed' }]);
// Two pins on the missing entity give one entry (Filbert N1).
const twoPins = crossWith(EYES, { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } });
assert.deepEqual((await readLibrary({ repo: r.repo, commit: r.commit, mapping: twoPins })).problems.filter(p => p.code === 'R8'),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', message: 'pinned value changed' }]);
// Changed, with the key's line; a missing key has no line.
assert.deepEqual(await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['eyes: green']) }),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', line: 4, message: 'pinned value changed' }]);
assert.deepEqual((await at({})).map(p => [p.path, p.line]), [['Book One/Characters/Mara Vell.md', undefined]]);
// A list holding the value, or null, is not the value (String(['grey']) would be 'grey').
for (const eyes of ['eyes: [grey]', 'eyes:', 'eyes: ~'])
assert.deepEqual((await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', [eyes]) })).map(p => p.line), [4], eyes);
// Unparsed (R3) front matter counts as changed.
assert.equal((await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['eyes: grey', 'bad: {a: 1}']) })).length, 1);
// A number pin compares as text.
const r2 = repo(t, { ...BASE, 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['age: 30']) });
const age = crossWith({ pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'age', value: 30 } });
assert.deepEqual((await readLibrary({ repo: r2.repo, commit: r2.commit, mapping: age })).problems.filter(p => p.code === 'R8'), []);
});
test('C2-15 R8: a hybrid version of the entity that sets the field to another value is reported on that line; a link alone is not a version', async t => {
const read = async files => {
const r = repo(t, { ...BASE, 'Book One/Characters/Mara Vell.md': MARA_GREY, ...files });
return (await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8');
};
// Same type and name, no link to the entity: a version.
assert.deepEqual(await read({ 'Crossover/Characters/Older Mara.md': character('Mara Vell', ['eyes: blue']) }),
[{ code: 'R8', path: 'Crossover/Characters/Older Mara.md', line: 4, message: 'pinned value changed' }]);
// No name on the version: the file names compare.
assert.deepEqual((await read({ 'Crossover/Characters/Mara Vell.md': character(null, ['eyes: blue']) })).map(p => [p.path, p.line]),
[['Crossover/Characters/Mara Vell.md', 3]]);
// A version that keeps the value, or doesn't set the field, is clean.
assert.deepEqual(await read({ 'Crossover/Characters/Older Mara.md': character('Mara Vell', ['eyes: grey']),
'Crossover/Characters/Young Mara.md': character('Mara Vell') }), []);
// Not versions: a place with the field, a different character that links her, a playground copy, a source file.
assert.deepEqual(await read({
'Crossover/Places/Mara Vell.md': ['---', 'type: place', 'name: Mara Vell', 'eyes: blue', '---', '', ''].join('\n'),
'Crossover/Characters/Ilse.md': character('Ilse', ['eyes: blue', 'relationships:', ' - "[[Mara Vell]]"']),
'Second World/Characters/Mara Vell.md': character('Mara Vell', ['eyes: blue']),
}), []);
});
test('R7: two paths that differ only in case are both left out', async t => {
const r = repo(t, BASE);
writeFileSync(join(r.root, 'twin'), 'twin\n');
+1 -1
View File
@@ -72,7 +72,7 @@ test('with --mapping the survey adds scope, resolver step and problem counts', a
assert.deepEqual(tail, [
'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',
'resolved root 2, vault-prefix 1, relative 2, shortest 3, unresolved 0',
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0, R8 0']);
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0']);
for (const s of NEVER) assert.ok(!out.stdout.includes(s), s);
});
-15
View File
@@ -527,18 +527,3 @@ test('C2-03b T5 helper: the named seat\'s user and file, never a link, 0644, ano
assert.ok(statSync(marker).isFile(), 'the swap ran');
assert.equal(readFileSync(file, 'utf8'), 'fixture-token-other-0047\n', 'the slot path names the other file now');
});
test('C2-15 sage-4389 (a): an excluded-link R6 is written to the sync state with the default message and reads back', async t => {
const w = await world(t);
// context.business.projects.books is this same entry, so the run checks the mapping with the hybrid in it.
w.entry.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude: 'Book One/Places' }] };
const next = w.push({ 'Crossover/Chapters/A.md': 'See [[Harbor Town]].\n' });
const { state, code } = await w.run();
assert.equal(code, null);
assert.equal(state.served, next);
const read = await w.state();
assert.equal(read.served, next);
assert.deepEqual(read.problems.filter(p => p.path === 'Crossover/Chapters/A.md'),
[{ code: 'R6', path: 'Crossover/Chapters/A.md', line: 1, message: 'canon file refers into another universe' }]);
});
+1 -1
View File
@@ -228,7 +228,7 @@ fi
say "business $BUSINESS validates"
# 8. What comes next. Root's steps run from root's own checkout.
say "done. Next: root runs case2-unit.sh --copy $COMMIT --business $BUSINESS (design 10 step 2c), then 2d and 3."
say "done. Next: root runs case2-unit.sh --copy $COMMIT --business $BUSINESS (add --proxy when the config has a proxy section; design 10 step 2c), then 2d and 3."
if [ "$PROFILE" = library ]; then
say "Then Jason places the library token in $SLOT (step 4), and <user> runs case2-precheck.sh --business $BUSINESS --library-status (step 4a)."
fi
+65 -22
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# system units a hub runs as (plus P1's proxy with --proxy), [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# [--profile factory|library] [--proxy] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
@@ -31,8 +31,8 @@
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# but the copy id, and all selected units must name one copy, so a re-pin or rollback
# re-renders them together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
@@ -41,6 +41,7 @@
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_PROXY_TEMPLATE default packages/cli/systemd/mosaic-proxy.service.in
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
@@ -53,10 +54,12 @@ SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
PROXY_TEMPLATE="${CASE2_PROXY_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-proxy.service.in}"
PROXY_UNIT=mosaic-proxy.service
BUS_UNIT=[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--proxy] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+@-]*) return 1 ;; esac; }
@@ -67,7 +70,7 @@ abs_plain() {
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PROXY=0 PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
@@ -78,6 +81,7 @@ while [ $# -gt 0 ]; do
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--proxy) PROXY=1; shift ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
@@ -150,6 +154,7 @@ fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
[ "$PROXY" = 0 ] || root_only "$PROXY_TEMPLATE" 'proxy template'
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
@@ -449,7 +454,10 @@ render() {
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
[ "$kind" != webui ] || add="$add${CAPS//$'\n'/\\n}\n"
if [ "$kind" = bus ] && [ "$PROXY" = 1 ]; then
text="$(printf '%s\n' "$text" | sed "/^Description=/a Wants=$PROXY_UNIT\\nAfter=$PROXY_UNIT")"
fi
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
@@ -517,7 +525,7 @@ check_unit() {
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[ "$kind" != bus ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
@@ -537,6 +545,11 @@ check_unit() {
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
elif [ "$kind" = proxy ]; then
one ExecStart "$NODE $copy/packages/model-proxy/src/run.mjs"
none EnvironmentFile
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
@@ -554,7 +567,16 @@ check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
PAIRS=("bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT")
if [ "$PROXY" = 1 ]; then
PROXY_TEXT="$(render proxy "$PROXY_TEMPLATE")" || exit 1
check_unit proxy "$PROXY_TEXT" "rendered $PROXY_UNIT" "$COPY"
PAIRS=("proxy:$PROXY_UNIT:PROXY_TEXT" "${PAIRS[@]}")
else
[ ! -e "$UNIT_DIR/$PROXY_UNIT" ] && [ ! -L "$UNIT_DIR/$PROXY_UNIT" ] \
|| die "installed $PROXY_UNIT requires --proxy; nothing was changed"
fi
for pair in "${PAIRS[@]}"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
@@ -569,13 +591,17 @@ for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
for old in "${ids[@]}"; do
[ "$old" = "${ids[0]}" ] || die "the installed units name different copies, ${ids[0]} and $old"
done
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
NAMES=("$BUS_NAME" "$WEBUI_UNIT")
[ "$PROXY" = 0 ] || NAMES=("$PROXY_UNIT" "${NAMES[@]}")
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
for name in "${NAMES[@]}"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
@@ -588,23 +614,40 @@ write_unit() {
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
for pair in "${PAIRS[@]}"; do
IFS=: read -r kind name var <<<"$pair"
write_unit "$name" "${!var}"
done
# Enable all selected units before starting proxy (if selected), bus, Console.
# Only the bus depends on the proxy; the Console calls the bus per request.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" enable "${NAMES[@]}" || die "systemctl enable ${NAMES[*]} failed; nothing is started"
if [ "$PROXY" = 1 ]; then
"$SYSTEMCTL" start "$PROXY_UNIT" || die "systemctl start $PROXY_UNIT failed; the bus and Console are not started"
fi
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
if [ "$PROXY" = 1 ]; then
echo "case2-unit: wrote $UNIT_DIR/$PROXY_UNIT, $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled all three"
if [ -z "$running" ]; then
echo "case2-unit: enabled and started $PROXY_UNIT, then $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
else
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
for name in "${NAMES[@]}"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
if [ "$PROXY" = 1 ]; then
echo "case2-unit: restart $PROXY_UNIT, $BUS_NAME and $WEBUI_UNIT together to put all three on copy $COPY_ID; until then they may run different copies"
else
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
fi