Compare commits

...
45 changed files with 2694 additions and 48 deletions
+432
View File
@@ -0,0 +1,432 @@
# Row 197 execution ledger
Plan: MANIFEST.md r2, approved by Webby ca6b32d66.
Build grant: sage-1390. Suites, image builds and paired proof are not granted.
Rebased five documentation commits onto origin/refactor aa1bd209e before code.
Existing worktree herb/row197 and plain node_modules symlink retained.
- T1 preflight/steps: in progress.
- T2 locks/status/recovery: pending.
- T3 image/Docker: pending.
- T4 fixtures/timing: pending.
- T5 evidence/acceptance: historical timing done; implementation pending.
Preflight interfaces: T1 RunPaths supplies T2 ownership and T3 image checks.
T2 run receipt supplies T4 lane/port state and T5 attribution. T3 image identity
is checked before Docker-bearing steps. T4 timings supply T5 pair summaries.
No interface conflict found. No product API or assertion changes are authorized.
Ruling: use this approved BUILD.md as the execution ledger, rather than adding
skill workspace paths outside the manifest. This preserves the reviewed path
boundary. Missing ledger detail would cost recovery time, not product behavior.
## Implementation checkpoint 2026-10-20T20:11:58Z
T1-T5 source is present within the approved manifest. Full acceptance is pending.
Fresh single-file checks: preflight/supervisor 6/6; state/recovery 11/11; image and
Docker guard 4/4; port leases/attribution/proof summary 3/3. No skips or failures.
Existing files checked singly: board serve; WebUI serve, preview, proof-cli,
tester-cli and service; harness pi and Claude. Stack-release R1 selected single
passed 1/1. WebUI service also passed 31/31 with lane-a leases and a 30-byte root.
All existing assert lines are unchanged. Shell syntax and diff checks pass.
Observed defects fixed during the single-file cycles: cancellation incorrectly
returned zero when a TERM handler exited zero; recovery did not supersede an
unclean terminal; status omitted the lock-before-receipt window. Evidence logs
retain the failing and passing observations. Source still has no commit because
applicable suites have not run. No image build, proof, push or live operation.
Ruling: use an instrumented single-lane author gate on a frozen dirty-source
clone before committing source. The new wrapper requires a clean candidate and
proven image receipt, while repository rules require green suites before source
commits. The author gate retains every suite and assertion, caps concurrency at
two, uses own port/state/temp/project roots, and guards Docker against builds.
Cost if wrong: the author gate fails; clean-candidate wrapper baselines and the
paired proof remain mandatory after source is verified and committed.
Ruling: publish the initial run directory atomically after the start receipt is
complete. This avoids a second lane observing a partially written start. A
crash before publication has started no test or mutable Docker operation.
Cost if wrong: a lane refuses or quarantines; no protected state is bypassed.
## Implementation review corrections, 2026-10-20T20:33Z
Webby-0029 reviewed the first implementation packet at f942bd7d4.
B1 identified missing manifest tests. B2 identified a test that leased both real
bands through a private lock namespace. The old author-gate clear was withdrawn
in herb-0019. SOURCE.json, BUILD.patch and gate.sh still describe the old frozen
clone. They are not a clear for the edited working tree. No author gate ran.
B2 correction: the helper test uses only the active lane and MOSAIC_GATE_STATE.
It compares the other band arithmetically and has no unasserted listener. The
occupied-port probe first obtains its own real lease, binds that port, and
asserts that its sentinel listener survives refusal. No test reserves another
lane's band. Exhaustion narrows only the test process's band to one owned port.
B1 coverage added within the existing approved paths. No case is dropped:
| Manifest case | Added check |
| --- | --- |
| T1 canonical and builder checkout, existing output | gate-lane.test: direct validateRun refusals |
| T1 92-byte socket and over-107-byte socket | exact design example with a distinct gt- test root; 108-byte refusal |
| T1 both candidates carry tooling | validateTooling compares each launcher file, including missing and changed cases |
| T1 duplicates and NODE_TEST_CONTEXT | repeated package/script fixture is retained; sanitized environment is exercised by both supervised children |
| T2 one-winner race and inherited FDs | two actual shell wrappers race; a controlled supervisor verifies held FDs, and its child checks that no FD points to either lock file |
| T2 same checkout and legacy claim refusal | activeConflicts uses synthetic process observations with a live lane and a sibling landing gate |
| T2 surviving child and interrupted recovery | locked recovery kills a child proven by the private root, preserves a foreign child, is killed during fake Docker cleanup, leaves quarantine, and resumes under a new fixture grant |
| T3 missing receipt and missing image | verifyImage refuses absent provenance and failing inspection |
| T3 tag changed mid-gate and at gate end | runGate now uses the same executePinnedSteps boundary exercised by the tests; the next step never runs after a changed ID |
| T3 mutable-tag rebuild | fake preparation returns a new image ID with identical input digest; the old receipt refuses it |
| T3 cold and warm counters | preparation records one build and its time; warm execution records zero builds; repeated verification never builds |
| T4 parallel test-file leases | two generated Node test files hold ports in the same lane/state; locks are checked during child execution and shutdown |
| T4 exhausted band, occupied port and foreign listener | a one-port process-local band refuses both a held lease and an existing sentinel listener; the listener survives |
| T4 owner death | a killed allocator releases only its own holder; a foreign lease remains held |
| T4 lease through child shutdown | parent probes the lock while the fixture test runs and after its child has stopped |
| T4 fixtureCacheEnv | replacement child environment writes only under separate tmp, jiti and cache directories |
| T5 unrelated session append | a separate process waits for a running-step barrier, appends under a fixture HOME, and remains unrelated in the resulting metadata attribution |
N1 removes the unused closed binding. All leases reserve an unbound endpoint;
existing closed options need no call-site change. N2 makes child TMPDIR a tmp
subdirectory. N3 normalizes ss interface scopes before checking port 9. N4 uses
gt- for unit run roots. N5 documents the canonical node_modules dependency.
N6 is recorded: row 200 also edits stack-release.test.mjs; the second landing
must rebase and rerun that file singly. Sage was told in herb-0019.
Before Sage's hold, expanded preflight tests passed 11/11, state tests 14/14,
image tests 7/7, and the pi/Claude singles passed 8/8 each with separate tmp
subdirectories. Later source/test edits still need verification. In particular,
the new parallel test-file probe had a cleanup-order defect and is unverified.
Its root cleanup ran before child shutdown. Cleanup now stops children before
removing their root. Output is retained for a useful failure report. The initial
FD probe also assumed Node did not reopen FD numbers; it now checks the actual
lock targets. The early passing fixture file covered child allocators, before
they were changed to actual generated test files. It is not acceptance of the
latest file.
Sage-1448 stopped all node --test runs at 20:30Z for Rocko's browser slot.
The remaining owned process tree was stopped at 20:30:32Z. Inspection found no
remaining owned test processes afterward. Edits and static inspection continue.
20 module syntax checks, shell syntax and git diff checks pass. Existing-file
assertion lines remain identical to HEAD. No test is run during the hold.
Next: after Sage passes Rocko's clear, verify the corrected files singly,
including a lane-env helper-file check. Then refresh the frozen clone and all
three packet artifacts, send their hashes, and request a new author-gate clear.
Suites, image builds and paired proofs still need their own grants. No source
commit, push, suite, real Docker build or paired proof has run in this phase.
## Refrozen review packet, 2026-10-20T20:41Z
Sage-1454 lifted the single-file hold. Fresh runs passed without failures or
skips: review-t1-resume.log 11/11, review-t2-resume.log 14/14,
review-t3-resume.log 8/8, review-t4-resume.log 8/8. The helper file also passed
8/8 with lane-a environment in review-t4-lane-a.log. Its retained state had only
an a port-lock namespace, no b directory, and every lock was released. Generated
fixture Node test entries explicitly use --test-concurrency=2. The child-test
probe now finishes and cleans up before removing its root. All 41 new tests pass.
B1 and B2 fixes are ready for Webby's review. No manifest case was dropped.
All 25 source paths were copied to a new frozen clone and compared by mode and
SHA-256. This supersedes the earlier withdrawn source packet:
- SOURCE.json: 91ba149f2fc039a8bed0e6d1ad5da44b80fb8cbec84d0fa4e38616040138596b
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
- BUILD.patch: 0b3871bd9848ca223c7cda50266debc04643e5d986eb7f0fece096aaf25ff43e
- Frozen checkout: /mnt/storage/scratch/mg197-a-d22dacd92735/wt
- Output: /mnt/storage/scratch/mg197-a-d22dacd92735/out
- Temporary root: /mnt/storage/scratch/ga-VPXhG7 (30 bytes)
- Compose project: mg197-a-d22dacd92735
After an explicit author-gate grant, run this command with Sage's actual ID:
```sh
env MOSAIC_GATE_GRANT=ACTUAL_SAGE_GRANT_ID bash /home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/agents/herb/work/queue-197/gate.sh > /mnt/storage/scratch/mg197-a-d22dacd92735/out/gate.log 2>&1
```
No gate or suite has started. Queue order from sage-1454 is 192, 201, 204,
200, Gander, then 197. No branch deletion is authorized or attempted. Any later
rebase uses a new branch name, and pr-merge.sh -d stays off under #426(a).
Whichever of rows 197 and 200 lands second must rerun stack-release singly.
Clean-candidate wrapper baselines, image preparation and paired proofs remain
separately gated acceptance work. There is still no source commit or push.
## Implementation approval and final notes, 2026-10-20T20:47Z
Webby-0031 approved r2 in REVIEW-impl-r2.md at 1f404c973. Their static review
verified every source hash/mode and patch result, and all B1/B2 cases. No queue
review request comment exists yet; Sage owns creating it if needed.
Folded all three optional notes into the same approved paths. The helper file's
standalone fallback now shares /mnt/storage/scratch/mosaic-gate-lanes with real
lane-a leases and leaves its stable lock files in place. Band assertions use the
imported BANDS. Both T5 polling children fail after five seconds if their barrier
is missed. README records the shared standalone namespace and deadlines.
Fresh single-file logs review-r3-fixtures.log and review-r3-lane-a.log both pass
8/8 with zero skips. The lane-a run has no b lock directory, and every lock was
released. Syntax and diff checks pass. No existing test assertions or timeouts
changed. Remaining source matches the 11/14/8-file checks from r2.
Refreshed all 25 source rows in the existing frozen checkout. New digests replace
the preceding r2 packet. gate.sh and its command are unchanged:
- SOURCE.json: 3584763642506e81a532c839bdd07b111d25daa87e35dbe441d8e8c01a2c2aab
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
- BUILD.patch: 55fe390da17ab926a0593d39e79561abe52756a1b1a2c756ec57f45d90f10fde
Author gate remains unstarted and queued for Sage's explicit grant. No source
commit, suite, image build, paired proof, push or branch deletion ran.
## R3 approval receipt
Webby-0033 approved r3 in REVIEW-impl-r3.md at a5643f9bd. Static review verified
the three packet hashes, all 25 source rows, and the patch result. Their review
accepts N1-N3; no additional hang-guard test is requested. No queue review request
comment exists yet, so Webby has not recorded a queue review.
Read-only verification after receipt confirms that the approved packet hashes
and all 25 worktree/frozen source hashes and modes still match. Source and gate
script stay frozen. The author gate has not started and still needs Sage's grant.
## Queue review round 1
Sage-1476 authorized moving row 197 to in-review before the author gate.
Created CANDIDATE.sha256 from the same 25 reviewed SOURCE.json rows. Its digest
is 6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
Ran the queue move from the canonical checkout with op herb-197-review-r3.
The move succeeded at revision 2395, opening round 1 on #1695. The request post
failed before send because MOSAIC_GITEA_CREDENTIAL_FILE is not set; revision 2396
records the failed outcome. Exit code 1 describes the failed post, not a failed
row transition. Read-back confirms state in-review and the correct candidate.
Sage will post as jarvis and give Webby the request comment number. No credential
lookup, alternate post or retry was attempted here.
SOURCE.json, BUILD.patch, gate.sh and all 25 frozen source rows remain unchanged.
Latest slot order is row 190 r2, 201, 204, 200, Gander, then Herb. No gate starts
before Sage sends an explicit grant ID.
## Follow-on assignment
Sage-1503 assigns row 215, Case 2 C2-04, to Herb after row 197 (queue rev 2455,
Gosalyn design r11). Read docs/plans/2026-10-20_case2-rows.md, C2-04 section,
at 5b8def2f1 when starting that row. Its required base is origin/refactor
305857c8f. This assignment does not replace row 197 or authorize its gate.
Keep row 197's approved source frozen while awaiting Sage's grant. Start row
215 only after row 197, using a new branch name under the existing fleet rule.
## Round 2 merge preparation on row 233
Sage-1757 replaces history replay with one candidate commit on 57f59e0c3.
The attempted history replay on herb/row197-r2-233 had already finished when
that instruction arrived. Its only conflict was the append-only BUILD-LOG tail.
That branch remains for history; none of its replayed commits is in this round.
The current branch is herb/row197-r3-233, directly from
57f59e0c399ce29b1bb85ca6af686fdc10eb195d.
Stash correction: shared refs/stash was used incorrectly. The exact entry
2767b88549dfd8c7e810908b5416876fa50cb571 is now kept on
herb/row197-r3-wip. After verifying stash@{0} still named it, only that entry was
dropped. The two other entries remain. No further stash creation, force push,
branch deletion or refs/stash mutation is planned. A separate working-copy
backup is /mnt/storage/scratch/herb197-merge-W4oiLg.
Sage-1760 requires preserving every landed hunk on every changed manifest path.
Checked each of the 25 r1 paths with git diff aa1bd209e 57f59e0c3 -- PATH.
Exactly three changed upstream. Their r2 diff against 57f59e0c3 is captured in
r2-merge-only.patch:
| Merged path | Only row 197 changes against landed base | Landed content retained |
| --- | --- | --- |
| packages/cli/tests/stack-release.test.mjs | leasePort and laneAfter imports; freePort now calls the lease helper | All landed tests, including row 233's tests after R15 and BACKUP_STORE checks |
| packages/webui/tests/service.test.mjs | leasePort and laneAfter imports; closedPort now calls the lease helper | SEND_REFUSAL permits --sso; default unit rendering removes the @CONFIG_ENV@ line |
| scripts/test-discord.sh | fixture PI_CODING_AGENT_DIR setup; optional two-worker nested Node cap | Row 190's absent scratch MOSAIC_CONFIG selection and caller-config restoration |
The other 22 paths equal the r1 reviewed source bytes and modes. r2-MERGE.json
lists every path, its r1/r2 digest, mode, upstream-change flag and equality result.
Source copies came from the hash-verified r1 frozen checkout. Original r1 digest:
6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
No existing assertion or timeout line differs from the landed base. Static
checks only: 20 module syntax checks, four shell syntax checks and git diff check
pass. BUILD-LOG.md equals 57f59e0c3 byte for byte and is outside this commit.
Sage explicitly requested the local candidate SHA before granting tests. This
commit is an untested preparation checkpoint, not gate evidence. No tests or
push are authorized yet. r2-TEST-FILES.txt lists the requested grant scope:
16 Node test files singly and scripts/test-discord.sh, test-foundation.sh and
test-queue.sh. The four stack-release consumers are stack-release.test.mjs,
stack-release-external.test.mjs, stack-upgrade.test.mjs and stack-instances.test.mjs.
The shared stack-release-fixture.mjs is exercised by its importing test files,
not run as a standalone test. This covers all 13 touched Node test files plus
three additional stack-release consumers and all three touched shell suites.
Current slot order from sage-1760: Gizmoduck, Launchpad, Darkwing, then Herb,
around 06:30Z. Wait for an explicit grant. After granted checks and authorized
push, build the new candidate manifest from that pushed tip and open the
merge-scoped round 2. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain r1
historical artifacts and are not a clear to run the merged candidate.
## Round 2 rebuilt on row 234
Sage-1776 names landing c771a36f4bd0b6c359282425c668996a90072f93.
Built a new branch, herb/row197-r4-234, directly from that landing. No history
replay, stash, force push, branch deletion, test run or push is involved.
The prior candidate a3b7cbde8 remains untested and is not the queued candidate.
Checked git diff --name-only 57f59e0c3 c771a36f4 against all 25 source paths.
Only packages/cli/tests/stack-release.test.mjs overlaps; the other 24 do not.
The full comparison is in r2-on234-OVERLAP.txt. The stack-release file starts
from the landing and keeps both T46 untimed entries, host_grants and hosts.
Its only changes against the landing are the leasePort and laneAfter imports
and freePort's call to the lease helper. r2-on234-merge-only.patch records those
hunks. Existing assertions and timeouts match the landing exactly.
Every other source path equals a3b7cbde8 byte for byte and retains its mode.
This includes the previously merged service and test-discord files, preserving
all landed --sso, CONFIG_ENV and scratch-config handling. The earlier evidence
r2-MERGE.json still records those three pre-234 merge cases. The new
r2-on234-MERGE.json records each current digest, prior digest, mode and equality
result. BUILD-LOG.md equals the new landing byte for byte and is excluded.
Static check of the changed Node file and the candidate diff pass. No tests
were run. The scope stays r2-TEST-FILES.txt: 16 Node files singly plus the three
shell suites. No manifest or queue round is rebuilt until granted checks and
authorized push. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain historical
r1 evidence. The test slot follows Darkwing 240 and needs Sage's explicit grant.
## Round 6 preparation (sage-1883, mos-1162)
New branch herb/row197-r6 starts at fefd54e13, after the standalone row
212 landing ee7b2f137. No r5 manifest path changed between e7078e6a8 and
this base; all 32 r5 blobs and modes were copied intact before the two
umask changes. No history replay, stash, force push or branch deletion.
executeSteps now launches each argv through /bin/bash with umask 022
and exec, preserving the process group and argv boundaries. The supervisor
and gate-lane.sh retain 077, including state, locks, private logs and roots.
New gate-lane tests assert the actual child umask and created file mode,
the unchanged parent mask, private log mode, and actual wrapper-created
state/lock modes. No shell, image preparation, credentials test or other
existing assertions changed. process.test.mjs equals the landed base;
lane-image.mjs equals r5. N7 remains DEFERRED at fefd54e13; rows 243 and
244 own the explicit chmod and PI_AUTH_FILE fixtures respectively.
Read Sage's permitted failed pkg-bus log: process.test startup token refusal
saw ready.ok true, consistent with inherited 077 narrowing its fixture mode.
No further diagnostic read of the retained ga-8I3iZc root is needed.
No r6 tests or probes have run. Source commit, push and new manifest await
the explicit granted checks. r6-OVERLAP.json records the empty overlap.
Sage-1892 focused grant: gate-lane.test whole 13/0, process.test whole
through executeSteps 8/0, zero skips/cancellations. Syntax checks passed.
All new umask/mode assertions passed. No Docker, image, lane or proof run.
Empty /proc cwd/argv scan and removal recorded in r6-granted/cleanup.json;
clear herb-0068 at 09:21Z. The source matches r5 on 29 of its 32 paths,
including modes. Only BUILD.md, gate-lane.test.mjs and gate-lane.mjs differ.
The new r6-OVERLAP.json records the base comparison. No process test edit.
Sage-1895 authorizes one source commit and seat-only push after range check.
## Round 7 preparation (sage-1951, sage-1955)
The r6 proof passed all package suites with the original process.test,
then failed script-release. The guard injected --no-build into compose run;
Compose 5.5.1 rejects it before container creation. The old unit test only
asserted the same invalid argv, so it could not detect this compatibility
error. This supersedes no-build-flag claims in the historical packet.
herb/row197-r7 starts at b1fece6f0. No r6 manifest path changed between
fefd54e13 and this base; all 33 r6 blobs/modes were copied intact before
r7 tests were authored. No history replay, stash, force push or deletion.
Sage-1955 accepts the private tempRoot/compose-no-build.yaml override,
mode0600, with build: !reset null and image pinned to the receipt's imageId.
The guard owns the -f arguments and forces run --pull never --no-deps.
Caller-supplied Compose config/build/pull options remain refused.
The existing proof removes successful temp roots and retains failed roots;
the override stays within that same cleanup ownership.
Tests first: installed-binary run --help before SERVICE checks parser
acceptance without starting a container. A separate compose config --format
json check uses the actual candidate compose.yaml, made-up env/auth paths,
and an empty Docker config directory, asserting no build key and the exact
pinned image. The no-build-restoration mutant must fail that parser test.
No test, probe or Docker command has run on r7; waiting for Sage's grant.
Primary references: Docker's merge reference documents !reset null:
https://docs.docker.com/reference/compose-file/merge/#reset-value
Docker's run reference documents --pull and --no-deps, with no --no-build:
https://docs.docker.com/reference/cli/docker/compose/run/
Installed 5.5.1 acceptance and merged configuration are still unverified.
Sage-1956 focused grant, 2026-10-21 11:19-11:21Z: busy check CLEAR.
Whole gate-lane-image.test on the r6 guard: 9 pass, 2 fail. The installed
Compose parser emitted unknown flag: --no-build; the other expected failure
was the missing override helper. No cancellation or skip. After the fix,
whole file 11/0. The committed r7-no-build.mutant.patch restores the invalid
flag. Applied only for the installed parser regression: 0/1 with the same
unknown-flag error, killed. Restored source and whole file passed 11/0 again.
Both approved node --check commands passed. Empty /proc cwd/argv scan;
clear herb-0073 at 11:21:04Z. No container start, image build, lane, proof or
full suite ran. Logs/counts/scan are in r7-granted/ (local run evidence).
The guard supplies both absolute -f paths, suppressing automatic override
selection, pins receipt imageId, removes build via !reset null and adds
--pull never --no-deps. Its main entry publishes the override before Docker
runs. A unique private staging file plus hard-link publication prevents
partial writes or replacement under concurrent guard calls; existing bytes,
mode0600, file type and ownership must match, with O_NOFOLLOW on reads.
The file stays under the run's short private tempRoot for existing proof
cleanup. Caller Compose options remain allowlisted and config/build/pull
options refuse. The actual installed Compose config test reports no build
key and the exact image ID; parser help is before SERVICE, so no container
operation can run. Fixtures supply made-up env/auth paths and empty Docker
configuration. Existing image provenance, cleanup and umask behavior remain.
Range check: one source commit on b1fece6f0, same 33 r6 paths plus the
mutant patch and r7 overlap receipt (35 paths). Thirty r6 paths retain the
same blob/mode. Only lane-docker.mjs, gate-lane-image.test.mjs and BUILD.md
differ from r6. process.test and BUILD-LOG equal the landing. No force push,
branch deletion, history replay or other source edits. Sage-1956 authorizes
the source commit and seat-only push; fresh proof remains Sage's action.
## Round 8 preparation (sage-1979)
The r7 D5 proof passed business and bus, then failed the two installed
Compose probes in pkg-cli. Their subprocess fixture retained the lane's
shim-leading PATH but omitted MOSAIC_GATE_DOCKER_RECORD, so the shim read
an undefined path. This is a test context error; the production guard stays
byte-identical to r7.
New herb/row197-r8 starts at 5c6f73ed7. None of r7's 35 manifest paths
changed from b1fece6f0 to this base. Tests choose the record's real absolute
Docker path when a lane record is set; outside lanes they keep using docker.
Only the two read-only help/config probes use this helper. The subprocess
env still holds fixture-only configuration; the record need not propagate
to it. No run or policy allowlist is widened.
Sage-1979 grants 215's 16 singles first, then r8's normal and lane-shaped
shim-leading PATH whole-file checks and the record-lookup-removal mutant.
The shim fixture invokes the actual unchanged guard, with a private made-up
record naming the real Docker binary. No image build, container start, lane
or proof is part of r8 checks. No r8 checks have started yet.
Sage-1979 r8 checks followed 215's early singles, then clear herb-0078 at
2026-10-21 12:09:28Z. Whole gate-lane-image.test normal PATH11/0; whole
actual guard-shim-leading PATH plus private record11/0. The committed
r8-record-lookup.mutant.patch removes the lookup: installed-Compose argv
regression0/1, killed with the original Received undefined path error.
Restored code, repeated whole shim-PATH file11/0. All zero skips/cancellations.
Logs/counts/fixture metadata/empty /proc cwd+argv scan are in r8-granted/.
Both 215 and r8 private roots were removed after an empty process scan.
No image build, container start, suite, lane or proof ran for r8.
Range: one commit on5c6f73ed7. Of r7's35 paths,33 keep the same blob/mode;
only gate-lane-image.test and this BUILD.md change. The r8 mutant and overlap
receipt are added. The production guard and all other tooling match r7.
BUILD-LOG and process.test equal the landing. No history replay, force push,
branch deletion, live state change or policy widening. Fresh proof remains
Sage's action after Webby's merge review.
+207
View File
@@ -0,0 +1,207 @@
{
"r1Base": "aa1bd209e",
"r1Candidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
"r2Base": "57f59e0c399ce29b1bb85ca6af686fdc10eb195d",
"rows": [
{
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"r2Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"r2Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-image.test.mjs",
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"r2Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-state.test.mjs",
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"r2Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane.test.mjs",
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"r2Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
"r2Sha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
"mode": "0o644",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "packages/control-board/tests/serve.test.mjs",
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"r2Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/harness/tests/claude-session.test.mjs",
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"r2Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/harness/tests/pi-session.test.mjs",
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"r2Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/preview.test.mjs",
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"r2Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/proof-cli.test.mjs",
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"r2Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/serve.test.mjs",
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"r2Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/service.test.mjs",
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
"r2Sha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"mode": "0o644",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "packages/webui/tests/tester-cli.test.mjs",
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"r2Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/test-discord.sh",
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
"r2Sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"mode": "0o755",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "scripts/test-foundation.sh",
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"r2Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/test-queue.sh",
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"r2Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/README.md",
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"r2Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/gate-lane.mjs",
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"r2Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/gate-lane.sh",
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"r2Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-docker.mjs",
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"r2Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-evidence.mjs",
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"r2Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-fixtures.mjs",
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"r2Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-image.mjs",
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"r2Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-state.mjs",
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"r2Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
}
]
}
@@ -0,0 +1,19 @@
packages/cli/tests/gate-lane-fixtures.test.mjs
packages/cli/tests/gate-lane-image.test.mjs
packages/cli/tests/gate-lane-state.test.mjs
packages/cli/tests/gate-lane.test.mjs
packages/cli/tests/stack-instances.test.mjs
packages/cli/tests/stack-release-external.test.mjs
packages/cli/tests/stack-release.test.mjs
packages/cli/tests/stack-upgrade.test.mjs
packages/control-board/tests/serve.test.mjs
packages/harness/tests/claude-session.test.mjs
packages/harness/tests/pi-session.test.mjs
packages/webui/tests/preview.test.mjs
packages/webui/tests/proof-cli.test.mjs
packages/webui/tests/serve.test.mjs
packages/webui/tests/service.test.mjs
packages/webui/tests/tester-cli.test.mjs
scripts/test-discord.sh
scripts/test-foundation.sh
scripts/test-queue.sh
@@ -0,0 +1,45 @@
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
index 8788158c3..a78f52062 100644
--- a/packages/cli/tests/stack-release.test.mjs
+++ b/packages/cli/tests/stack-release.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -1777 +1779 @@ const TESTER='7499';
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
index a6e66608a..adb460d07 100644
--- a/packages/webui/tests/service.test.mjs
+++ b/packages/webui/tests/service.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -48,9 +50 @@ function filled(flags, root = REPO) {
-async function closedPort() {
- const server = createServer();
- server.listen(0, '127.0.0.1');
- await once(server, 'listening');
- const { port } = server.address();
- server.close();
- await once(server, 'close');
- return port;
-}
+async function closedPort() { return leasePort({after:laneAfter}, {closed:true}); }
diff --git a/scripts/test-discord.sh b/scripts/test-discord.sh
index 114994936..4daf0d143 100755
--- a/scripts/test-discord.sh
+++ b/scripts/test-discord.sh
@@ -73,0 +74,2 @@ TOOLS_JSON="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"ma
+export PI_CODING_AGENT_DIR="$SANDBOX/pi-agent"
+mkdir -p "$PI_CODING_AGENT_DIR"
@@ -145 +147,8 @@ fi
-node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
+node_tests() {
+ local lane_concurrency=()
+ if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
+ [ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || return 2
+ lane_concurrency=(--test-concurrency=2)
+ fi
+ env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" "$@"
+}
@@ -0,0 +1,235 @@
{
"base": "c771a36f4bd0b6c359282425c668996a90072f93",
"priorCandidate": "a3b7cbde8ef51c206a83769a7c272e4f9a1de5bc",
"originalCandidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
"overlap": [
"packages/cli/tests/stack-release.test.mjs"
],
"rows": [
{
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"priorCandidateSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"newSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"priorCandidateSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"newSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-image.test.mjs",
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"priorCandidateSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"newSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-state.test.mjs",
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"priorCandidateSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"newSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane.test.mjs",
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"priorCandidateSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"newSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
"priorCandidateSha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
"newSha256": "071ea2803b8c7e0450bb4c86ac9b49ae7c180af1599dca52bfce6d0274ca205b",
"unchangedFromPrior": false,
"overlaps234": true,
"mode": "0o644"
},
{
"path": "packages/control-board/tests/serve.test.mjs",
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"priorCandidateSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"newSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/harness/tests/claude-session.test.mjs",
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"priorCandidateSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"newSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/harness/tests/pi-session.test.mjs",
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"priorCandidateSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"newSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/preview.test.mjs",
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"priorCandidateSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"newSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/proof-cli.test.mjs",
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"priorCandidateSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"newSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/serve.test.mjs",
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"priorCandidateSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"newSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/service.test.mjs",
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
"priorCandidateSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"newSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/tester-cli.test.mjs",
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"priorCandidateSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"newSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/test-discord.sh",
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
"priorCandidateSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"newSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/test-foundation.sh",
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"priorCandidateSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"newSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/test-queue.sh",
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"priorCandidateSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"newSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/testing/README.md",
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"priorCandidateSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"newSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/gate-lane.mjs",
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"priorCandidateSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"newSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/gate-lane.sh",
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"priorCandidateSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"newSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/testing/lane-docker.mjs",
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"priorCandidateSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"newSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-evidence.mjs",
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"priorCandidateSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"newSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-fixtures.mjs",
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"priorCandidateSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"newSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-image.mjs",
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"priorCandidateSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"newSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-state.mjs",
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"priorCandidateSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"newSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
}
]
}
@@ -0,0 +1,47 @@
Compared 57f59e0c3 to c771a36f4bd0b6c359282425c668996a90072f93
agents/gizmoduck/work/queue-234/BUILD.md
docs/SESSIONS.md
packages/bus/README.md
packages/bus/schema.sql
packages/bus/src/broker.mjs
packages/bus/src/credentials.mjs
packages/bus/src/host-grant.mjs
packages/bus/src/host-run.mjs
packages/bus/src/migrations/3i-3j.sql
packages/bus/src/runtime.mjs
packages/bus/src/server.mjs
packages/bus/src/store.mjs
packages/bus/tests/account-migration.test.mjs
packages/bus/tests/chief-of-staff.test.mjs
packages/bus/tests/comment-migration.test.mjs
packages/bus/tests/fixtures/schema-3i.sql
packages/bus/tests/grant-enroll.test.mjs
packages/bus/tests/grant-migration.test.mjs
packages/bus/tests/hard-waits.test.mjs
packages/bus/tests/host-fixtures.mjs
packages/bus/tests/host-run.test.mjs
packages/bus/tests/host-tools.test.mjs
packages/bus/tests/name-migration.test.mjs
packages/bus/tests/project-runtime.test.mjs
packages/bus/tests/schema.test.mjs
packages/bus/tests/spec-fixtures.mjs
packages/bus/tests/spec-migration.test.mjs
packages/bus/tests/thread-migration.test.mjs
packages/bus/tests/usage-migration.test.mjs
packages/business/src/business.mjs
packages/business/src/hosts.mjs
packages/business/tests/hosts.test.mjs
packages/cli/README.md
packages/cli/src/cli.mjs
packages/cli/src/hosts.mjs
packages/cli/tests/hosts.test.mjs
packages/cli/tests/stack-release.test.mjs
scripts/mosaic
tools/host/mosaic-keys
tools/host/mosaic-keys-write
tools/host/mosaic-keys.sshd.conf
tools/host/mosaic-keys.sudoers
Overlap with row 197 source manifest:
packages/cli/tests/stack-release.test.mjs
Other 24 source paths: no overlap.
@@ -0,0 +1,10 @@
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
index daae6efb7..910785bc4 100644
--- a/packages/cli/tests/stack-release.test.mjs
+++ b/packages/cli/tests/stack-release.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -1779 +1781 @@ const TESTER='7499';
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
@@ -0,0 +1,5 @@
{
"base": "fefd54e13",
"prior": "9aa6d52d9",
"overlaps": []
}
@@ -0,0 +1,6 @@
{
"base": "b1fece6f0",
"r6": "df0c59608",
"r6Base": "fefd54e13",
"overlaps": []
}
@@ -0,0 +1,11 @@
--- a/scripts/testing/lane-docker.mjs
+++ b/scripts/testing/lane-docker.mjs
@@ -34,7 +34,7 @@
const service=argv.indexOf('mosaic-agent',2);
if(service<0||argv.slice(2,service).some(a=>!['--rm','-T','--no-deps','--interactive','-i'].includes(a)))refuse('Docker Compose run options not allowed');
const [base,override]=composeFiles(run);
- return ['compose','-f',base,'-f',override,'run','--pull','never','--no-deps',...argv.slice(2)];
+ return ['compose','-f',base,'-f',override,'run','--no-build','--pull','never','--no-deps',...argv.slice(2)];
}
refuse('Docker mutation or unowned command');
}
@@ -0,0 +1,6 @@
{
"base": "5c6f73ed7",
"r7": "9c3166915",
"r7Base": "b1fece6f0",
"overlaps": []
}
@@ -0,0 +1,11 @@
--- a/packages/cli/tests/gate-lane-image.test.mjs
+++ b/packages/cli/tests/gate-lane-image.test.mjs
@@ -85,7 +85,7 @@
// including when the lane's guard is first on PATH. Child fixture env stays
// isolated; passing the real absolute path avoids re-entering that guard.
function installedDocker(){
- const file=process.env.MOSAIC_GATE_DOCKER_RECORD;
+ const file=undefined; // mutant: lose the lane's real-binary lookup
if(!file)return 'docker';
const binary=JSON.parse(readFileSync(file,'utf8')).docker;
assert.ok(typeof binary==='string'&&isAbsolute(binary),'lane record must name the real absolute Docker binary');
+230
View File
@@ -0,0 +1,230 @@
# Row 244: gate runs use a fixture pi auth file
Brief: `docs/plans/2026-10-21_gate-env-rows.md` § "Gate runs use a fixture pi auth file" (Sage, sage-1909).
Reviewer Webby. DEFERRED entry "Gate runs mount the host's pi auth file" (059577886). Design notes
written before row 197 lands, read against `origin/herb/row197-r6` (df0c59608). Recheck every line cite
against 197's landing before code starts.
## Where Compose is reached
`compose.yaml:43` mounts `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}` read-only. The default is
a literal path, not `$HOME`, so keeping `HOME` in the lane env doesn't change it.
Gate steps come from `discoverSteps` (`gate-lane.mjs:32`): one `pkg-<name>` step per package with tests
(not queue) and one `script-<name>` step per `scripts/test-*.sh`. A grep of those for `run-task.sh`,
`docker compose`, `hello.sh`, `verify.sh`, `agent.sh` and `release.sh activate` finds two Compose
paths. The other hits (`cohort.test.mjs` "hello" shim requests, `test-auth.sh`) don't reach Compose.
| Step | Path to Compose | Adapter |
|---|---|---|
| `test-task.sh` :150-385 | `scripts/run-task.sh run` → `mosaic-task.mjs:396` `docker compose run` with `spawnEnv = {...process.env}` | mock (`mock_config`) |
| `test-release.sh` :69, :85, :93 | `release.sh activate` → `health_check` (:41-55) → `run-task.sh run tasks/hello-marker.json` | **pi** (config :61 sets no adapter; provider zai, model glm-5.3-flash) |
`mosaic-task.mjs` passes `process.env` through, so a `PI_AUTH_FILE` exported by a suite reaches Compose.
## The conflict: test-release's healthy activation is a live provider call
"healthy activation succeeds" (:85) and "repeat activation succeeds" (:93) need `health_check` to
pass. That runs the real pi adapter, asks glm-5.3-flash for `MOSAIC_HELLO_OK`, and authenticates through
the mounted auth file. The lane's `cleanEnv` drops `ZAI_API_KEY`, so the mount is the only credential.
With a fixture auth file the provider call fails, and four checks fail: :85, the pointer check after it,
:93 and "log is append-only". So every gate run today spends a real provider call on the host credential.
Options put to Sage (morgana-0037):
- **A (recommended).** test-release's config sets `"adapter":"mock"`, and the two healthy activations
run with `MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK`. The fault injection still refuses, because its task
expects `MOSAIC_FAULT_<random>`. The suite keeps proving the release state machine, the image and
Compose path, and the pointer and log. It stops proving that the pi image can reach the provider.
Real `release.sh activate` runs still do that, with the real credential. No credential and no spend
in the gate.
- **B.** Keep the live call. The lane passes the real auth file explicitly. That contradicts the brief
and DEFERRED, so I don't take it without a ruling.
**Ruling: A (sage-1914, 09:51Z).** B keeps a live billed call on every gate, which is what this row
stops. Sage's conditions:
1. test-release.sh's suite config sets `"adapter":"mock"`, with a comment beside it saying why: no
credential and no provider call in the gate (row 244). The healthy activations run with
`MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK`.
2. Fault injection still refuses. It keeps the random `MOSAIC_FAULT_<n>` expectation, and the mock's
fixed reply can't satisfy it. Mutant M-F forces the fault task's expected value to `MOSAIC_HELLO_OK`.
It must fail the suite.
3. The fake-docker test also asserts that test-release's activations ran with the mock adapter. The
fake docker records `MOSAIC_ADAPTER`, which `mosaic-task.mjs` sets in `spawnEnv` from the resolved
config. Then a suite drifting back to pi fails the test.
4. Coverage dropped: test-release.sh no longer proves that the pi image reaches the provider. A real
`release.sh activate` still proves it, with the real credential. Sage adds a DEFERRED line at landing
if Webby wants one.
guardDocker's new refusal also gets a mutant (M-G, below).
## Change
1. `scripts/test-task.sh` and `scripts/test-release.sh`: right after `SANDBOX` is made, write
`$SANDBOX/pi/auth.json` (`{}`, mode set by `chmod 600` after the write, so it doesn't depend on the
umask, row 243) and `export PI_AUTH_FILE`. It has to be a regular file that exists, because a missing
bind source makes Docker create a directory on the host. The suites set it themselves, so the legacy
gate (outside the lane) is covered too.
2. `scripts/test-release.sh`: option A above.
3. `scripts/testing/gate-lane.mjs` `cleanEnv`: set `PI_AUTH_FILE` to `join(o.tempRoot,'pi-auth.json')`,
and `run` writes that file (`{}`, `flag:'wx'`, then chmod 0600) next to where it writes `bin/docker`.
Any later step that reaches Compose without its own fixture then gets the lane's fixture.
4. `scripts/testing/lane-docker.mjs` `guardDocker`: `compose run` also refuses unless `PI_AUTH_FILE` is
set, resolves inside `run.tempRoot` and isn't the host default (refusal text
`Compose auth mount not a lane fixture`). The guard already sees every Docker call a lane step makes,
so this is where "no gate step mounts the host file" is enforced at run time. The CLI entry passes
`process.env`.
5. `compose.yaml` is unchanged (brief: not in scope).
6. At landing: the DEFERRED entry moves to Done with the landing commit.
## Tests
In `packages/cli/tests/gate-lane.test.mjs` (row 197's file; the exact file follows 197's landing):
- `guardDocker` refuses `compose run` with `PI_AUTH_FILE` unset, set to the host default, or set outside
`tempRoot`; it accepts the lane fixture and a suite fixture under `tempRoot`. Other verbs are unchanged.
- `cleanEnv` returns a `PI_AUTH_FILE` inside `tempRoot` that isn't the host default.
- The suites, read through their Compose environment: run `test-task.sh` and `test-release.sh` with a
fake `docker` first on `PATH`. It answers `info` and `image inspect` with 0, and for `compose run` it
appends `PI_AUTH_FILE` and whether that file is a regular 0600 file to a record. Then it prints the
mock response. The test ignores the suites' own exit code and asserts that the record has at least one
`compose run` from each suite and that every entry names an existing fixture file, never
`/home/jwoltje/.pi/agent/auth.json`. The test never reads the host file. If a full suite run with the
fake docker turns out too slow or brittle, I'll say so in the review request, with timings, and fall
back to the guard and `cleanEnv` tests plus a static check that each suite exports `PI_AUTH_FILE`
before its first Compose call. I won't drop it silently.
Mutants (each must fail a test; run on Sage's slot):
- M-F: the fault task's `expectExact` forced to `MOSAIC_HELLO_OK` in `release.sh` `health_check`. Must
fail `test-release.sh` ("fault-injected activation refuses").
- M-G: the `guardDocker` `PI_AUTH_FILE` check removed. Must fail the guard test.
- M-A: test-release's config back to no adapter (pi). Must fail the fake-docker test (condition 3).
- M-L: `cleanEnv` stops setting `PI_AUTH_FILE`. Must fail the `cleanEnv` test.
Gate: those tests, `test-task.sh` and `test-release.sh` whole, the 197 lane tests, and the four
mutants, all on Sage's slot.
## Files (expected)
`scripts/test-task.sh`, `scripts/test-release.sh`, `scripts/testing/gate-lane.mjs`,
`scripts/testing/lane-docker.mjs`, `packages/cli/tests/gate-lane.test.mjs` (or a new
`gate-lane-auth.test.mjs`), `docs/plans/DEFERRED.md` (at landing), `scripts/testing/README.md` if it
lists the guard's rules.
## Correction (morgana-0040, 18:50Z)
The table above says `test-task.sh` uses only the mock adapter. That was wrong. On a36523713 two
sections run the pi adapter through `$CONFIG` (provider zai) and authenticate through the host mount:
`:443-501` (live hello, result.json written and correct, wrong expectExact, distinct run dirs, list,
fork base, fork child, fork recall, ancestor untouched, child branch file) and `:518-530` (user recall
run, recalled the user name). That's 13 checks and about 6 billed provider calls per gate run. I missed
them because I read the mock section and stopped there.
**Ruling C (sage-2061, 18:50Z).** D (keep them live) refused.
1. The run-record checks (hello, result.json, wrong expectExact, distinct run dirs, list) move to the
mock adapter. Every assertion is kept. Only the response source changes.
2. Fork recall and user recall run only under `MOSAIC_LIVE_PROVIDER=1`. Otherwise the suite prints
`skip live provider cases (row 244: no provider call in the gate)`, which the lane's requiredSkips
regex doesn't match, on purpose. A test asserts the line prints when the variable is unset and that
the live checks run, and fail under a fixture, when it's 1 (M-T).
3. The dropped coverage and the DEFERRED wording are below. Sage writes the entry at landing.
## Build (on a36523713, sage-2056)
Rechecked against 197's landing: `lane-docker.mjs:30` guardDocker, `gate-lane.mjs:109` cleanEnv and
`:181` (bin/docker written), `compose.yaml:43`, `release.sh:41-55` health_check, `mosaic-task.mjs:321-322`
spawnEnv and MOSAIC_ADAPTER, `test-release.sh:61` config. All hold.
- `scripts/testing/lane-docker.mjs`: `guardDocker(argv,run,env=process.env)`. After the option check, a
`compose run` calls `laneAuthFile`. It refuses (`Compose auth mount not a lane fixture`) unless
`PI_AUTH_FILE` is absolute, isn't the host default, resolves (realpath) inside the realpath of
`run.tempRoot`, and is a regular file. Symlinks out of the root and relative paths refuse. `info` and
`image inspect` need no auth file. The CLI entry passes `process.env`.
- `scripts/testing/gate-lane.mjs`: `cleanEnv` sets `PI_AUTH_FILE=join(o.tempRoot,'pi-auth.json')`.
`runGate` writes it (`{}`, flag `wx`, then `chmodSync` 0600) right after `bin/docker`.
`MOSAIC_LIVE_PROVIDER` isn't on cleanEnv's keep list, so the lane can't request live cases.
- `scripts/test-release.sh`: exports a `$SANDBOX/pi/auth.json` fixture (`{}`, `chmod 600` after the
write, exits 1 if the write fails). The config adds `"adapter":"mock"` with the ruling A comment. The
fault, healthy and repeat activations all run with `MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK`, so the
fault activation refuses only because its `MOSAIC_FAULT_<n>` expectation is wrong. (Correction,
before any run: the first build gave the fault activation no mock response. The mock then answered
empty, which fails any expectation, so M-F would have survived. Fixed in the follow-up commit.)
- `scripts/test-task.sh`: same fixture, exported only when `MOSAIC_LIVE_PROVIDER` isn't 1, so an
operator's live run keeps their own `PI_AUTH_FILE` (or Compose's default). Ruling C (a) and (b) as
above. The section header changes from "live: real runs" to "run records through the mock adapter",
the check name "live hello task..." becomes "hello task...", and the docker-less branch of the new
split prints `skip run-record cases (docker unavailable)`.
- `packages/cli/tests/gate-lane-image.test.mjs` (:21, :106, :119): the compose-run calls pass
`run.env`, the composeFixture env, which already holds a 0600 `PI_AUTH_FILE` inside tempRoot (accepted
by sage-2061).
- `scripts/testing/README.md`: one sentence on the lane fixture and the refusal.
- New `packages/cli/tests/gate-lane-auth.test.mjs`, five tests:
1. guardDocker accepts the lane fixture and a suite fixture under tempRoot. It refuses unset, empty,
the host default, a file outside, a symlink inside that points outside, a missing path, a directory
and a relative path. The option check still comes first. `info` and `image inspect` pass with no env.
2. cleanEnv sets the lane fixture path and drops `MOSAIC_LIVE_PROVIDER` and the caller's
`PI_AUTH_FILE`.
3. test-release.sh in a private copy of the checkout, with a fake `docker` first on PATH. The fake
answers every verb with 0. For `compose run` it records `PI_AUTH_FILE`, `stat -c '%F %a'` of it
(never for the host path) and `MOSAIC_ADAPTER`, and it prints `MOSAIC_MOCK_RESPONSE` when the
adapter is mock. Asserts: at least 3 compose runs, every one on a regular 0600 file under the
suite's TMPDIR, and every adapter mock (condition 3). The healthy and fault lines print OK.
4. test-task.sh, same harness: every compose run on its fixture and the mock adapter, the skip line
printed exactly twice, and the hello check OK.
5. M-T: test-task.sh with `MOSAIC_LIVE_PROVIDER=1` and an operator-style fixture. No skip line,
`FAIL fork base: teach succeeds` and `FAIL recalled user name`, at least one pi run, and every run
on the operator's file.
The copy leaves out `.git`, `node_modules` (symlinked), `v1`, `agents`, `docs`, `BUILD-LOG.md` and
`scripts/foundation`, about 14 MB. Copying is needed because `test-release.sh` rewrites `RELEASE`
while it runs, which would race other cli tests that read it. The suites ignore their own exit codes
here: the fake doesn't emit the mock adapter's env lines, so some seam checks fail by design.
- Readers: `gate-lane.mjs` is imported by gate-lane.test, gate-lane-state.test,
gate-lane-fixtures.test, gate-lane-image.test and fixtures/gate-lane-child.mjs (unchanged exports).
`lane-docker.mjs` by gate-lane-image.test. No test hash-pins any changed file. vars.test.mjs names
`scripts/test-task.sh` only as a string value.
Coverage dropped from the gate (for Webby, and for the DEFERRED entry):
- `test-release.sh` no longer proves that the pi image reaches the provider (ruling A).
- `test-task.sh` no longer proves session fork recall or onboarding user recall against a real model
(ruling C).
DEFERRED wording for Sage (sage-2061 condition 3): "**Real-model suite cases run outside the gate.**
Row 244 moved `test-task.sh` fork recall (M11) and user recall (M16) behind `MOSAIC_LIVE_PROVIDER=1`,
and `test-release.sh` activations to the mock adapter. Before any re-pin whose tree changes
`packages/harness` or the session fork code (`scripts/mosaic-task.mjs` session and `sessionForkFrom`
handling, the pi adapter), an operator runs `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the
real pi credential and a real `release.sh activate`, and records both results on the re-pin row."
Mutants (each must fail a named test; on Sage's slot):
- M-F: `release.sh` health_check fault task `expectExact` forced to `MOSAIC_HELLO_OK`. Fails
test-release.sh "fault-injected activation refuses", and test 3's OK match. This needs the fault
activation's `MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK` (see the correction above).
- M-G: the `laneAuthFile(env,run)` call removed from guardDocker. Fails test 1.
- M-A: `,"adapter":"mock"` removed from test-release's config. Fails test 3 (adapter set is pi).
- M-L: the `PI_AUTH_FILE` key removed from cleanEnv. Fails test 2.
- M-T: test 5 itself (sage-2061). As a mutant check too: the `MOSAIC_LIVE_PROVIDER` gate in test-task
forced on fails test 4 (skip line count and pi adapter).
Results, Sage's slot (sage-2147), candidate 81e037d20, 2026-10-21 20:46:16Z to 20:46:59Z. Busy check
CLEAR before and after. Image `mosaic-poc-agent:0.85.1-r0.0.12` present, so nothing was built. No
unexpected failure. Singles ran under umask 022 with TMPDIR under /mnt/storage/scratch/tmp.
| Run | rc | Result | Time |
|---|---|---|---|
| gate-lane-auth.test.mjs (new) | 0 | 5 pass, 0 fail | 14 s |
| gate-lane-image.test.mjs | 0 | 11 pass, 0 fail | 0 s |
| gate-lane.test.mjs | 0 | 13 pass, 0 fail | 0 s |
| gate-lane-state.test.mjs | 0 | 14 pass, 0 fail | 2 s |
| gate-lane-fixtures.test.mjs | 0 | 8 pass, 0 fail | 0 s |
| test-task.sh | 0 | OK 91, FAIL 0, the two live-skip lines | 12 s |
| test-release.sh | 0 | OK 14, FAIL 0, Docker section on the mock adapter | 2 s |
Mutants, each run on its named gate-lane-auth test and restored after:
| Mutant | Target | Result |
|---|---|---|
| M-G | test 1 | killed (0 pass, 1 fail) |
| M-L | test 2 | killed (0 pass, 1 fail) |
| M-A | test 3 | killed (0 pass, 1 fail) |
| M-T | test 4 | killed (0 pass, 1 fail), 6 s |
| M-F | test 3 | killed (0 pass, 1 fail) |
| M-F | real test-release.sh | killed: rc 1, OK 11, FAIL 3, the first `fault-injected activation refuses (exit 0, expected 1)` |
@@ -0,0 +1,17 @@
# Row 197 proof observations (Sage)
Kept here, not in Herb's BUILD.md, because that file is one of row 197's 37 paths and the landing check
(mos-1174, mos-1185 item 5) requires them equal to c45ddeb6b.
## Open observation (mos-1185 item 3), 2026-10-21
Proofs 4 and 5 each stopped on a test outside 197's paths, in a pair wave, and both times in lane b:
- Proof 4, pair1-b, pkg-conversation, K12 (`cohort.test.mjs:292`): `populated` undefined. Row 250, Vikunja #450.
- Proof 5, pair2-b, pkg-bus, T27 (`host-run.test.mjs:469`): no identities right after `ssh-add -t 1`. Vikunja #453.
Mos checked the step log birth times: the twin step started in both lanes within half a second each time
(proof 4 pkg-conversation a 11:03:59.343, b .855 CDT; proof 5 pkg-bus a 12:07:25.354, b .573). Waves run in
lockstep, so that holds for every pair wave, the passing ones too. Lane b is the twin that starts later.
Mos's reading: contention exposing fragile tests, not a 197 defect. Paired lanes add contention, and that
is 197's cost, covered by #451.
+1
View File
@@ -1419,3 +1419,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-21T06:47:54Z | researcher | row 228 (#1729) round 3 in review (rev 2702; start rev 2700) | rebase of r2 onto the 234 landing c771a36f4 as researcher/row228-r3 cf18c42b9 (one commit, pushed) with Webby's round 2 N1 (:711 and :718); manifest 82cf98c6 (3 files); slot sage-1796: session.test 13 + host.test 42 = 55/0, manifest OK before and after, busy228.sh copy named in the clear; r2 94/0 and M1 killed stand (seat blobs unchanged); request attempt left in requesting state (lock held by sage-238-review-2b when the outcome rev was written, exit 3; transport said failed pre-send, no token), Sage resolves and posts as jarvis; packet agents/researcher/work/queue-228/r3
2026-10-21T07:29:23Z | researcher | row 228 (#1729) landed as 86249ac55 (pushed 07:25Z by Sage) | patch-id b6ffe31feb0d matches my r3 candidate cf18c42b9 (checked here against origin/refactor); Sage combined gate with 238: seat 165/0, cli 640/0, scripts 585/0; queue note rev 2725, done rev 2727, #1729 closed comment 29966 (sage-1824); q228r2 worktree removed, branches researcher/row228, -r2 and -r3 stay on origin and locally; packets agents/researcher/work/queue-228 r1 r2 r3 uncommitted
2026-10-21T15:19:14Z | Sage (Opus 5.5, 1ef1e4f8) | MVP push 10-21 afternoon: rows 249, 197 proof 3/4, 222 singles, 66 r4d, queue-commit | 249 landed 9de5a57ab (+7b9737953 DEFERRED N1, #1751 closed, Vikunja 445 c442); 197 proof 3 rc=1 on a driver defect, Herb's fix a5b636750 to Webby, proof 4 set up in sage-r197p4; 222 singles 34/34 and 5 mutants killed (sage-2028); 66 round 4 in review (82c830779, c30083/30084); queue revs 2642-2904 committed 0636721e6
2026-10-21T15:20:58Z | Filbert (Opus 5.5, 048f3c63) | row 66 r4 review (sage-2037, sage/row66-r4d 82c830779, manifest 6cfd812da4ca) | APPROVE 30086, rev 2906; static, no tests; rebase only, row 66 +/- lines identical to r3 next to rows 212/213/232/245; N1 code-only patch-id d67275ac4251 (508bb684eade includes BUILD.md), N2 BUILD.md cap-sentence wording; packet-only notes
@@ -0,0 +1,48 @@
# Brief: K12 reads cgroup.events after cgroup.kill under load
Sage, 2026-10-21, for Mos's ruling mos-1182 item 3. Vikunja #450. Gate owner Sage. Owner and reviewer are
named when the row is briefed. Base: refactor at the brief commit. The row comes after row 197, unless
proof 5 fails on K12 again (mos-1182 item 4). Then it goes ahead of 197, and 197's candidates rebase on it.
## Why
`packages/conversation/tests/cohort.test.mjs` K12 (line 268) force-stops a member that forks in a loop.
Then it asks the shim for `events` and asserts `ev.populated === 0` (line 292). It never checks `ev.ok`.
`events()` in `packages/conversation/src/shim.mjs:83-96` returns `{ ok: false, unavailable }` when
`engine/cgroup.events` can't be read or has no `populated` field. Under load the test then fails on
`undefined !== 0` and never says why.
This has happened twice:
- BUILD-LOG.md:4092: conversation went 181/1 at a load of about 12 while mutants ran, and 182/0 alone.
- Row 197 proof 4, wave pair1-b, 2026-10-21T16:04Z: 147/1 with two conversation suites running at once.
The log is `/mnt/storage/scratch/sage-r197p4/proof-run/pair1-b/out/pkg-conversation.log`.
## Steps, in order
1. Make the failure name its cause. The K12 assertion prints `ev` (with `ok` and `unavailable`) when it
fails. Commit this first, on its own.
2. Find why the file is unreadable or lacks `populated` after `cgroup.kill` under load, and fix that at the
source. Use the shim, the cohort controller or the test's setup and teardown, whichever the evidence points
to. Record the evidence in `agents/OWNER/work/queue-ROW/BUILD.md`.
3. Do not loosen the assertion. Do not treat `unavailable` as empty, or `populated` as 0 when it can't be
read. Retrying a read that is still in progress is allowed only if step 2 shows a race in the read
itself, and the retry has a bound and a test.
## Files owned
`packages/conversation/src/shim.mjs`, `packages/conversation/src/cohort.mjs` if step 2 needs it, and
`packages/conversation/tests/cohort.test.mjs`. Nothing in row 197's 37 paths.
## Tests
- K12 still asserts `populated` 0 and an empty member list.
- A new case shows `events` returning `unavailable` with its reason, and that the controller treats that as
unavailable, not as empty.
- The conversation suite passes whole three times in a row, and once while a second conversation suite
runs at the same time. That is the load that broke it in proof 4.
## Gate
The reviewer approves on the row, naming the candidate manifest. The conversation suite and the
control-board suite pass whole on a gate rerun in a clean worktree, on Sage's slot.
@@ -0,0 +1,55 @@
# Brief: T27 lists no identities right after ssh-add -t 1
Sage, 2026-10-21, for Mos's ruling mos-1185 item 2. Vikunja #453. Gate owner Sage. Owner and reviewer are
named when the row is briefed. Base: refactor at the brief commit. The row comes after row 197, unless
proof 6 fails on a test outside 197's paths in a pair wave (mos-1185 item 4). Then this row and row 250 go
ahead of 197, and 197's candidates rebase on them. The pattern is row 250's
(`docs/plans/2026-10-21_k12-cohort-events.md`).
## Why
`packages/bus/tests/host-run.test.mjs:453`, "T27: ssh-add -t drops the key from an agent the test keeps
alive", starts its own `ssh-agent -D -a SOCK`, calls `addKey(sock, key, 1)` from
`packages/bus/src/host-run.mjs:87`, and asserts that `ssh-add -l` lists the key (line 469). Then it sleeps
2.5 s and asserts the key is gone.
Row 197 proof 5, wave pair2-b, 2026-10-21T17:07Z: the first assertion got "The agent has no identities."
31.9 ms into the test. The bus suite went 289/1. The log is
`/mnt/storage/scratch/sage-r197p5/proof-run/pair2-b/out/pkg-bus.log`. The same test passed in solo-b and
pair1-b of that run.
The cause is not known yet. One lead, unverified (Mos, mos-1185): ssh-agent may count lifetimes in whole
seconds of CLOCK_MONOTONIC, so a key added just before a monotonic second boundary expires within
milliseconds. A test aligned to wall-clock seconds did not test that idea.
Sage checked the lead on dragon-lin, OpenSSH 10.5p1, 2026-10-21T17:19Z (`/mnt/storage/scratch/sage-r64/t27-mono.mjs`,
outside any gate). It made 80 adds of `ssh-add -t 1`, each at a chosen fraction of a CLOCK_MONOTONIC second
(`process.hrtime`), and listed at once. 8 of 80 missed. Every miss was an add made at 0.9947-0.9973 of a
monotonic second that finished at 0.998-0.0008. No add earlier in the second missed. So a 1-second lifetime
can end within milliseconds of the add. Step 2 still confirms this against the T27 failure itself.
## Steps, in order
1. Make the failure name its cause. When the first assertion fails, print the time from addKey's start
to its resolve and to the `ssh-add -l` call, the monotonic clock (`process.hrtime.bigint()` or
`/proc/uptime`) at each, and the agent's state (alive, socket present). Commit this first, on its own.
2. Find the cause and fix it at the source: the test's lifetime choice, `addKey`, or how the agent is
started. Record the evidence in `agents/OWNER/work/queue-ROW/BUILD.md`.
3. Do not loosen the check. The test still proves the key is listed after the add and gone after its
lifetime. A longer lifetime is a fix only if step 2 shows whole-second rounding is the cause, and then
the sleep grows with it.
## Files owned
`packages/bus/tests/host-run.test.mjs`, and `packages/bus/src/host-run.mjs` only if step 2 points there.
Nothing in row 197's 37 paths.
## Tests
- T27 still asserts the key is listed, then gone.
- The bus suite passes whole three times in a row, and once while a second bus suite runs at the same time.
## Gate
The reviewer approves on the row, naming the candidate manifest. The bus suite passes whole on a gate
rerun in a clean worktree, on Sage's slot.
+20
View File
@@ -0,0 +1,20 @@
// Controlled child for lane crash tests; no live service, network or model.
import { writeFileSync } from 'node:fs';
const [mode,marker]=process.argv.slice(2);
if(marker&&mode!=='lock-probe')writeFileSync(marker,String(process.pid));
if(mode!=='lock-probe')process.stdout.write('READY\n');
if(mode==='ignore-term')process.on('SIGTERM',()=>{});
else process.on('SIGTERM',()=>process.exit(0));
setInterval(()=>{},1000);
if(mode==='lock-probe'){
const stateRoot=marker;
const {verifyLocks,executeSteps}=await import('../../../../scripts/testing/gate-lane.mjs');
verifyLocks({lane:'a',stateRoot,exclusive:false});
const {appendRecord,identity}=await import('../../../../scripts/testing/lane-state.mjs');
const {mkdirSync}=await import('node:fs');const {join}=await import('node:path');
const out=join(stateRoot,'out');mkdirSync(out);
appendRecord(join(stateRoot,'runs','abcdef123456'),{kind:'start',lane:'a',owner:identity()});
const result=await executeSteps([{name:'fds',cwd:stateRoot,argv:[process.execPath,'-e',"const fs=require('fs'); for(const fd of fs.readdirSync('/proc/self/fd')){let p;try{p=fs.readlinkSync('/proc/self/fd/'+fd)}catch{continue}if(p.endsWith('/a.lock')||p.endsWith('/image.lock'))process.exit(7)}"]}],{out,env:process.env,record:()=>{}});
if(result.rc)process.exit(result.rc);
writeFileSync(join(stateRoot,'ready'),'verified');
}
+100
View File
@@ -0,0 +1,100 @@
// Row 244: no gate step mounts the host's pi auth file or makes a provider call.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, rmSync, cpSync, readFileSync, existsSync, realpathSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { guardDocker } from '../../../scripts/testing/lane-docker.mjs';
import { cleanEnv } from '../../../scripts/testing/gate-lane.mjs';
const REPO = realpathSync(fileURLToPath(new URL('../../../', import.meta.url)));
const HOST_AUTH = '/home/jwoltje/.pi/agent/auth.json';
const LIVE_SKIP = 'skip live provider cases (row 244: no provider call in the gate)';
const scratch = t => { const root = mkdtempSync('/mnt/storage/scratch/gt-'); t.after(() => rmSync(root, { recursive: true, force: true })); return root; };
const fixture = (dir, name = 'auth.json') => { const file = join(dir, name); writeFileSync(file, '{}'); chmodSync(file, 0o600); return file; };
const RUN = ['compose', 'run', '--rm', '-T', 'mosaic-agent', 'prompt'];
test('guardDocker refuses a compose run unless PI_AUTH_FILE is a regular file inside the lane temp root', t => {
const tempRoot = scratch(t), outside = scratch(t);
const run = { tempRoot, checkout: join(tempRoot, 'checkout'), imageId: 'sha256:' + 'a'.repeat(64), tag: 'mosaic-poc-agent:fixture' };
const lane = fixture(tempRoot, 'pi-auth.json');
mkdirSync(join(tempRoot, 'suite', 'pi'), { recursive: true });
const suite = fixture(join(tempRoot, 'suite', 'pi'));
for (const file of [lane, suite]) assert.equal(guardDocker(RUN, run, { PI_AUTH_FILE: file })[0], 'compose');
const link = join(tempRoot, 'escape.json'); symlinkSync(fixture(outside), link);
mkdirSync(join(tempRoot, 'dir.json'));
for (const env of [{}, { PI_AUTH_FILE: '' }, { PI_AUTH_FILE: HOST_AUTH }, { PI_AUTH_FILE: fixture(outside) }, { PI_AUTH_FILE: link },
{ PI_AUTH_FILE: join(tempRoot, 'missing.json') }, { PI_AUTH_FILE: join(tempRoot, 'dir.json') }, { PI_AUTH_FILE: relative(process.cwd(), lane) }])
assert.throws(() => guardDocker(RUN, run, env), /Compose auth mount not a lane fixture/, JSON.stringify(env));
// The option check still comes first, and the other allowed verbs need no auth file.
assert.throws(() => guardDocker(['compose', 'run', '--build', 'mosaic-agent'], run, {}), /options not allowed/);
assert.deepEqual(guardDocker(['info'], run, {}), ['info']);
assert.deepEqual(guardDocker(['image', 'inspect', run.tag], run, {}), ['image', 'inspect', run.tag]);
});
test('cleanEnv points PI_AUTH_FILE at the lane fixture and drops MOSAIC_LIVE_PROVIDER', t => {
const tempRoot = scratch(t), saved = { PI_AUTH_FILE: process.env.PI_AUTH_FILE, MOSAIC_LIVE_PROVIDER: process.env.MOSAIC_LIVE_PROVIDER };
process.env.PI_AUTH_FILE = HOST_AUTH; process.env.MOSAIC_LIVE_PROVIDER = '1';
let env;
try { env = cleanEnv({ tempRoot, lane: 'a', stateRoot: join(tempRoot, 'state'), out: join(tempRoot, 'out') }, { project: 'fixture' }, '/fake/docker'); }
finally { for (const [k, v] of Object.entries(saved)) if (v === undefined) delete process.env[k]; else process.env[k] = v; }
assert.equal(env.PI_AUTH_FILE, join(tempRoot, 'pi-auth.json'));
assert.equal(env.MOSAIC_LIVE_PROVIDER, undefined);
});
// Both suites run in a private copy of the checkout: test-release.sh rewrites RELEASE while it runs.
// A fake docker records each compose run's PI_AUTH_FILE and MOSAIC_ADAPTER, and answers as the mock adapter.
// It never reads or stats the host auth file.
const SKIP_COPY = new Set(['.git', 'node_modules', 'v1', 'agents', 'docs', 'BUILD-LOG.md', join('scripts', 'foundation')]);
function suiteLane(t) {
const root = scratch(t), checkout = join(root, 'c'), bin = join(root, 'bin'), tmp = join(root, 'tmp');
cpSync(REPO, checkout, { recursive: true, filter: src => !SKIP_COPY.has(relative(REPO, src)) });
symlinkSync(realpathSync(join(REPO, 'node_modules')), join(checkout, 'node_modules'));
mkdirSync(bin); mkdirSync(tmp, { mode: 0o700 });
writeFileSync(join(bin, 'docker'), `#!/bin/bash
[ "$1" = compose ] || exit 0
f="\${PI_AUTH_FILE-}"
if [ -z "\${PI_AUTH_FILE+x}" ]; then kind=unset; elif [ "$f" = ${HOST_AUTH} ]; then kind=host; else kind="$(stat -c '%F %a' -- "$f" 2>/dev/null || echo missing)"; fi
printf '%s\\t%s\\t%s\\n' "$f" "$kind" "\${MOSAIC_ADAPTER-}" >> "$FAKE_DOCKER_RECORD"
[ "\${MOSAIC_ADAPTER-}" = mock ] && printf '%s\\n' "\${MOSAIC_MOCK_RESPONSE-}"
exit 0
`);
chmodSync(join(bin, 'docker'), 0o700);
return (script, extra = {}) => {
const record = join(root, `${script}-${Object.keys(extra).length}.tsv`), begin = performance.now();
const env = { PATH: `${bin}:${process.env.PATH}`, HOME: process.env.HOME, TMPDIR: tmp, LANG: 'C.UTF-8', FAKE_DOCKER_RECORD: record, ...extra };
const r = spawnSync('bash', [join(checkout, 'scripts', script)], { env, encoding: 'utf8', timeout: 600000 });
t.diagnostic(JSON.stringify({ script, extra: Object.keys(extra), wallMs: Math.round(performance.now() - begin), status: r.status }));
const runs = existsSync(record) ? readFileSync(record, 'utf8').trim().split('\n').filter(Boolean).map(l => { const [file, kind, adapter] = l.split('\t'); return { file, kind, adapter }; }) : [];
return { out: r.stdout + r.stderr, runs, tmp };
};
}
const fixtureRuns = (runs, tmp) => runs.every(r => r.file !== HOST_AUTH && r.file.startsWith(tmp + '/') && r.kind === 'regular file 600');
test('test-release.sh activates through the mock adapter with a 0600 fixture auth file', t => {
const { out, runs, tmp } = suiteLane(t)('test-release.sh');
assert.ok(runs.length >= 3, `fault, healthy and repeat activations reach Compose\n${out}`);
assert.ok(fixtureRuns(runs, tmp), JSON.stringify(runs));
assert.deepEqual([...new Set(runs.map(r => r.adapter))], ['mock'], 'ruling A: no pi adapter in the gate');
assert.match(out, /OK +healthy activation succeeds/); assert.match(out, /OK +fault-injected activation refuses/);
});
test('test-task.sh mounts only its fixture, uses only the mock adapter and prints the live skip twice', t => {
const { out, runs, tmp } = suiteLane(t)('test-task.sh');
assert.ok(runs.length >= 1, out);
assert.ok(fixtureRuns(runs, tmp), JSON.stringify(runs));
assert.deepEqual([...new Set(runs.map(r => r.adapter))], ['mock']);
assert.equal(out.split('\n').filter(l => l === LIVE_SKIP).length, 2, out);
assert.match(out, /OK +hello task succeeds with exact marker/);
});
test('M-T: MOSAIC_LIVE_PROVIDER=1 runs the real-model cases, which fail under a fixture auth file', t => {
const lane = suiteLane(t), auth = join(scratch(t), 'operator-auth.json'); writeFileSync(auth, '{}'); chmodSync(auth, 0o600);
const { out, runs } = lane('test-task.sh', { MOSAIC_LIVE_PROVIDER: '1', PI_AUTH_FILE: auth });
assert.ok(!out.includes(LIVE_SKIP), out);
// The fake answers a pi run with nothing: the fork base misses REMEMBERED, recall misses the name.
assert.match(out, /FAIL fork base: teach succeeds/); assert.match(out, /FAIL recalled user name/);
assert.ok(runs.some(r => r.adapter === 'pi'), JSON.stringify(runs));
assert.ok(runs.every(r => r.file === auth), 'live mode keeps the operator\'s PI_AUTH_FILE');
});
@@ -0,0 +1,94 @@
import test from 'node:test';import assert from 'node:assert/strict';
import { mkdtempSync,rmSync,mkdirSync,writeFileSync } from 'node:fs';import { join } from 'node:path';import { createServer } from 'node:net';import { once } from 'node:events';
const api=await import('../../../scripts/testing/lane-fixtures.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
const evidence=await import('../../../scripts/testing/lane-evidence.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
test('parallel lease holders use only the active lane and its shared state',async t=>{
const ctx=leaseContext(t),lane=ctx.lane;
const [a,b]=await Promise.all([api.leasePort(ctx.fixture,ctx),api.leasePort(ctx.fixture,ctx)]);
const bands=BANDS,[lo,hi]=bands[lane];
assert.ok(a>=lo&&a<=hi);assert.ok(b>=lo&&b<=hi);assert.notEqual(a,b);
assert.ok(bands.a[1]<bands.b[0]);assert.equal(bands.a[1]-bands.a[0],127);assert.equal(bands.b[1]-bands.b[0],127);
});
test('unrelated home changes do not become lane writes, and own-root references do',()=>{
assert.equal(typeof evidence.attributeEntries,'function','attribution is required');
const result=evidence.attributeEntries([{path:'/home/person/.claude/projects/other-session',link:null},{path:'/home/person/.cache/lane-abcdef123456',link:null},{path:'/home/person/.pi/item',link:'/mnt/storage/scratch/gt-ABCDEF/work'}],{nonce:'abcdef123456',tempRoot:'/mnt/storage/scratch/gt-ABCDEF',fixturePaths:[]});
assert.equal(result.owned.length,2);assert.equal(result.unrelated.length,1);
});
test('proof summaries refuse missing timings, non-overlap and a pair without a lane swap',()=>{
const solos=[{candidate:'one',wallMs:100},{candidate:'two',wallMs:100}];
const make=(lane,candidate,startMs=0,endMs=120)=>({lane,candidate,startMs,endMs,rc:0,clean:true});
const pairs=[[make('a','one'),make('b','two',5,125)],[make('b','one'),make('a','two',5,125)],[make('a','one'),make('b','two',5,125)]];
const result=evidence.summarizePairs(solos,pairs);assert.equal(result.length,3);assert.equal(result[0].makespanMs,125);assert.equal(result[0].runs[0].slowdown,1.2);assert.equal(result[0].throughput,1.6);
const bad=structuredClone(pairs);delete bad[0][0].endMs;assert.throws(()=>evidence.summarizePairs(solos,bad),/timing/);
const noSwap=structuredClone(pairs);noSwap[1]=structuredClone(pairs[0]);assert.throws(()=>evidence.summarizePairs(solos,noSwap),/swap/);
const serial=structuredClone(pairs);serial[0][1].startMs=130;serial[0][1].endMs=250;assert.throws(()=>evidence.summarizePairs(solos,serial),/overlap/);
});
import { BANDS } from '../../../scripts/testing/lane-state.mjs';
import { spawn,spawnSync } from 'node:child_process';
import { readFileSync,existsSync } from 'node:fs';
function leaseContext(t){
const lane=process.env.MOSAIC_GATE_LANE??'a',stateRoot=process.env.MOSAIC_GATE_STATE??'/mnt/storage/scratch/mosaic-gate-lanes',callbacks=[];
mkdirSync(stateRoot,{recursive:true,mode:0o700});
t.after(async()=>{for(const f of callbacks.reverse())await f();});
return {lane,stateRoot,fixture:{after:f=>callbacks.push(f)},cleanup:f=>callbacks.push(f)};
}
const delay=ms=>new Promise(r=>setTimeout(r,ms));
async function waitFor(check){for(let i=0;i<100;i++){if(check())return;await delay(20);}throw Error('fixture not ready');}
test('an exhausted single-port band refuses, and an occupied foreign listener survives the probe',async t=>{
const ctx=leaseContext(t),callbacks=[];
const port=await api.leasePort({after:f=>callbacks.push(f)},ctx),original=BANDS[ctx.lane];
const server=createServer();
try{
BANDS[ctx.lane]=[port,port];
await assert.rejects(api.leasePort(t,ctx),/exhausted/);
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(port,'127.0.0.1',resolve);});
// Release only our lock. The sentinel listener remains foreign to the next lease.
await callbacks.pop()();
await assert.rejects(api.leasePort(t,ctx),/exhausted/);assert.equal(server.listening,true);
}finally{BANDS[ctx.lane]=original;if(server.listening)await new Promise(r=>server.close(r));for(const f of callbacks)await f();}
});
test('parallel child allocators share real lane locks; a lease remains held through child shutdown',async t=>{
const ctx=leaseContext(t),root=mkdtempSync('/mnt/storage/scratch/gf-');
const modulePath=new URL('../../../scripts/testing/lane-fixtures.mjs',import.meta.url).href;
const code=`import test from 'node:test';import fs from 'node:fs';const {leasePort}=await import(${JSON.stringify(modulePath)});test('lease held until fixture child finishes',async t=>{const port=await leasePort(t,JSON.parse(process.env.LEASE_CONTEXT));fs.writeFileSync(process.env.LEASE_PORT_FILE,String(port));await new Promise(r=>{const timer=setInterval(()=>{if(fs.existsSync(process.env.LEASE_STOP_FILE)){clearInterval(timer);r()}},5)});});`;
const start=number=>{const file=join(root,`lease-${number}.test.mjs`),portFile=join(root,`port-${number}`),stopFile=join(root,`stop-${number}`);writeFileSync(file,code);const env={...process.env,LEASE_CONTEXT:JSON.stringify(ctx),LEASE_PORT_FILE:portFile,LEASE_STOP_FILE:stopFile};delete env.NODE_TEST_CONTEXT;const child=spawn(process.execPath,['--test','--test-concurrency=2','--test-reporter=tap',file],{env,stdio:['ignore','pipe','pipe']});child.output='';child.stderr.on('data',b=>child.output+=b);child.stdout.on('data',b=>child.output+=b);child.done=new Promise(r=>child.once('exit',code=>{child.exitedCode=code;r(code);}));child.portFile=portFile;child.stopFile=stopFile;return child;};
const children=[start(1),start(2)];ctx.cleanup(async()=>{try{for(const c of children)writeFileSync(c.stopFile,'stop');await Promise.all(children.map(c=>c.done));}finally{rmSync(root,{recursive:true,force:true});}});
await waitFor(()=>{for(const c of children)if(c.exitedCode!==undefined)throw Error(`allocator exited ${c.exitedCode}: ${c.output}`);return children.every(c=>existsSync(c.portFile));});
const ports=children.map(c=>Number(readFileSync(c.portFile,'utf8')));assert.notEqual(...ports);
const lock=join(ctx.stateRoot,'ports',ctx.lane,`${ports[0]}.lock`);
assert.equal(spawnSync('flock',['-n',lock,'true']).status,1);
writeFileSync(children[0].stopFile,'stop');assert.equal(await children[0].done,0,children[0].output);
assert.equal(spawnSync('flock',['-n',lock,'true']).status,0);
assert.equal(spawnSync('flock',['-n',join(ctx.stateRoot,'ports',ctx.lane,`${ports[1]}.lock`),'true']).status,1);
});
test('abrupt allocator death releases its holder without touching a foreign lease',async t=>{
const ctx=leaseContext(t),foreign=await api.leasePort(ctx.fixture,ctx),modulePath=new URL('../../../scripts/testing/lane-fixtures.mjs',import.meta.url).href;
const code=`const {leasePort}=await import(${JSON.stringify(modulePath)});const port=await leasePort({after:()=>{}},JSON.parse(process.argv[1]));console.log(port);setInterval(()=>{},1000);`;
const child=spawn(process.execPath,['--input-type=module','-e',code,JSON.stringify(ctx)],{stdio:['ignore','pipe','pipe']});const done=new Promise(r=>child.once('exit',r));ctx.cleanup(async()=>{child.kill('SIGKILL');await done;});
const port=await new Promise(r=>child.stdout.once('data',b=>r(Number(String(b).trim()))));assert.notEqual(port,foreign);
child.kill('SIGKILL');await done;
const lock=join(ctx.stateRoot,'ports',ctx.lane,`${port}.lock`);
await waitFor(()=>spawnSync('flock',['-n',lock,'true']).status===0);
assert.equal(spawnSync('flock',['-n',join(ctx.stateRoot,'ports',ctx.lane,`${foreign}.lock`),'true']).status,1);
});
test('fixture caches survive a replacement environment with a separate tmp subdirectory',async t=>{
const root=mkdtempSync('/mnt/storage/scratch/gf-');t.after(()=>rmSync(root,{recursive:true,force:true}));
const env=api.fixtureCacheEnv(root);assert.equal(env.TMPDIR,join(root,'tmp'));assert.equal(env.JITI_FS_CACHE,join(root,'jiti'));assert.equal(env.XDG_CACHE_HOME,join(root,'cache'));
const code="const fs=require('fs'),p=require('path');for(const k of ['TMPDIR','JITI_FS_CACHE','XDG_CACHE_HOME'])fs.writeFileSync(p.join(process.env[k],k),'fixture');if(process.env.HOME)process.exit(7)";
const result=spawnSync(process.execPath,['-e',code],{env,encoding:'utf8'});assert.equal(result.status,0,result.stderr);
for(const key of Object.keys(env))assert.equal(readFileSync(join(env[key],key),'utf8'),'fixture');
});
test('an unrelated session can append while a supervised step runs without becoming an attributed write',async t=>{
const root=mkdtempSync('/mnt/storage/scratch/gf-');t.after(()=>rmSync(root,{recursive:true,force:true}));
const home=join(root,'home'),out=join(root,'out'),file=join(home,'.claude','other-session');mkdirSync(join(home,'.claude'),{recursive:true});mkdirSync(out);writeFileSync(file,'before');
const before=evidence.homeMetadata(home),prior=new Map(before.map(e=>[e.path,JSON.stringify(e)]));
const {executeSteps}=await import('../../../scripts/testing/gate-lane.mjs');
const barrier=join(root,'step-running');
const append=spawn(process.execPath,['-e',"const fs=require('fs');const deadline=setTimeout(()=>{console.error('append barrier timed out');process.exit(8)},5000);const timer=setInterval(()=>{if(fs.existsSync(process.argv[2])){clearInterval(timer);clearTimeout(deadline);fs.appendFileSync(process.argv[1],'unrelated append')}},5)",file,barrier],{stdio:'ignore'});const appended=new Promise(r=>append.once('exit',r));t.after(()=>append.kill('SIGKILL'));
let appendedDuringStep=false;
const stepCode="const fs=require('fs');fs.writeFileSync(process.argv[1],'running');const deadline=setTimeout(()=>{console.error('session append timed out');process.exit(8)},5000);const timer=setInterval(()=>{if(fs.readFileSync(process.argv[2],'utf8').includes('unrelated append')){clearInterval(timer);clearTimeout(deadline)}},5)";
const result=await executeSteps([{name:'wait',cwd:root,argv:[process.execPath,'-e',stepCode,barrier,file]}],{out,env:process.env,record:r=>{if(r.kind==='step')appendedDuringStep=readFileSync(file,'utf8').includes('unrelated append');}});
assert.equal(await appended,0);assert.equal(result.rc,0);assert.equal(appendedDuringStep,true);
const changed=evidence.homeMetadata(home).filter(e=>prior.get(e.path)!==JSON.stringify(e));assert.ok(changed.some(e=>e.path===file));
const attributed=evidence.attributeEntries(changed,{nonce:'abcdef123456',tempRoot:join(root,'temp'),fixturePaths:[]});assert.equal(attributed.owned.length,0);assert.ok(attributed.unrelated.length>0);
});
+136
View File
@@ -0,0 +1,136 @@
import test from 'node:test';import assert from 'node:assert/strict';
import { mkdtempSync,rmSync,writeFileSync,mkdirSync,cpSync,readFileSync,statSync,symlinkSync,unlinkSync } from 'node:fs';import { join, isAbsolute } from 'node:path';
import { spawnSync } from 'node:child_process';
const image=await import('../../../scripts/testing/lane-image.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
const docker=await import('../../../scripts/testing/lane-docker.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
test('a changed COPY input or actual image ID refuses compatible image sharing',t=>{
assert.equal(typeof image.imageFingerprint,'function','image fingerprint required');
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
writeFileSync(join(root,'RELEASE'),'0.0.1\n');
for(const d of ['contracts','src','adapters']){mkdirSync(join(root,d));writeFileSync(join(root,d,'one'),'same');}
const digest=image.imageFingerprint(root),id='sha256:'+'a'.repeat(64);const receipt={inputDigest:digest,imageId:id,tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:'node@sha256:'+'b'.repeat(64),platform:'linux/amd64'};
const inspect=()=>({Id:id,Os:'linux',Architecture:'amd64'});
assert.equal(image.verifyImage(receipt,root,inspect).imageId,id);
assert.throws(()=>image.verifyImage(receipt,root,()=>({...inspect(),Id:'sha256:'+'c'.repeat(64)})),/image ID/);
writeFileSync(join(root,'adapters','one'),'different');assert.throws(()=>image.verifyImage(receipt,root,inspect),/inputs/);
});
test('warm Docker guard disables implicit build and pull and refuses image mutation',t=>{
assert.equal(typeof docker.guardDocker,'function','Docker guard required');
const run=composeFixture(t);
assert.deepEqual(docker.guardDocker(['compose','run','--rm','-T','mosaic-agent','prompt'],run,run.env),['compose','-f',join(run.checkout,'compose.yaml'),'-f',join(run.tempRoot,'compose-no-build.yaml'),'run','--pull','never','--no-deps','--rm','-T','mosaic-agent','prompt']);
for(const args of [['build','.'],['image','prune'],['compose','build'],['tag','a','b'],['network','rm','foreign']])assert.throws(()=>docker.guardDocker(args,run),/refused/);
});
test('exclusive image preparation records actual image ID and build wall time and requires a grant',t=>{
assert.equal(typeof image.prepareImage,'function','prepareImage is required');
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d));
const calls=[];const invoke=(args)=>{calls.push(args);if(args[0]==='compose')return '';return JSON.stringify(args[2]==='node:24-bookworm-slim'?{RepoDigests:['node@sha256:'+'b'.repeat(64)]}:{Id:'sha256:'+'a'.repeat(64),Os:'linux',Architecture:'amd64'});};
assert.throws(()=>image.prepareImage(root,{grant:'',tempRoot:root},invoke),/grant/);
const result=image.prepareImage(root,{grant:'fixture',tempRoot:root},invoke);assert.equal(result.imageId,'sha256:'+'a'.repeat(64));assert.equal(result.builds,1);assert.ok(result.buildWallMs>=0);assert.ok(calls.some(x=>x.join(' ')==='compose build'));
});
test('cleanup refuses a foreign Docker label and removes only an exact owned project',()=>{
const project='mg197-a-abcdef123456',id='123456789abc';let removed=false,label='foreign';
const fake=(args)=>{if(args[0]==='ps')return removed?'':id;if(args[0]==='network')return '';if(args[0]==='container'&&args[1]==='inspect')return label;if(args[0]==='container'&&args[1]==='rm'){removed=true;return '';}throw Error('unexpected command');};
// The injectable executor exercises ownership checks without touching Docker.
assert.throws(()=>docker.cleanupProject({project},fake),/foreign/);assert.equal(removed,false);
label=project;assert.deepEqual(docker.cleanupProject({project},fake).removed,[{kind:'container',id}]);assert.equal(removed,true);
});
function imageFixture(t){
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d));
const receipt={inputDigest:image.imageFingerprint(root),imageId:'sha256:'+'a'.repeat(64),tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:'node@sha256:'+'b'.repeat(64),platform:'linux/amd64'};
return {root,receipt};
}
test('missing receipt and missing image refuse without permitting a gate',t=>{
const {root,receipt}=imageFixture(t);assert.throws(()=>image.verifyImage(null,root),/provenance receipt/);
assert.throws(()=>image.verifyImage(receipt,root,()=>{throw Error('No such image');}),/No such image/);
assert.throws(()=>image.verifyImage({...receipt,tag:'mosaic-poc-agent:wrong'},root,()=>({Id:receipt.imageId,Os:'linux',Architecture:'amd64'})),/release identity/);
});
test('a tag changed between supervised steps fails the next boundary and suppresses gate success',async t=>{
const {root,receipt}=imageFixture(t),out=join(root,'out');mkdirSync(out);
let actual=receipt.imageId;const inspect=()=>({Id:actual,Os:'linux',Architecture:'amd64'}),events=[];
const {executePinnedSteps}=await import('../../../scripts/testing/gate-lane.mjs');
await assert.rejects(executePinnedSteps([
{name:'first',cwd:root,argv:[process.execPath,'-e','process.exit(0)']},
{name:'second',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}
],{out,env:process.env,record:r=>events.push(r)}, {receipt,checkout:root,inspect,afterStep:()=>actual='sha256:'+'c'.repeat(64)}),/image ID/);
assert.equal(events.filter(e=>e.kind==='step').length,1);
assert.equal(events.some(e=>e.kind==='terminal'&&e.rc===0),false);
});
test('mutable-base rebuilds with identical files have distinct image IDs; cold and warm counters stay separate',t=>{
const {root}=imageFixture(t);let builds=0,id='sha256:'+'a'.repeat(64);
const fake=args=>{if(args[0]==='compose'){builds++;return 'fixture build';}return JSON.stringify(args[2]==='node:24-bookworm-slim'?{RepoDigests:['node@sha256:'+'b'.repeat(64)]}:{Id:id,Os:'linux',Architecture:'amd64'});};
const cold=image.prepareImage(root,{grant:'fixture-cold',tempRoot:root},fake);
assert.equal(cold.builds,1);assert.ok(cold.buildWallMs>=0);assert.equal(builds,1);
for(let i=0;i<3;i++)image.verifyImage(cold,root,()=>({Id:id,Os:'linux',Architecture:'amd64'}));
assert.equal(builds,1,'warm verification builds zero images');
id='sha256:'+'c'.repeat(64);const rebuilt=image.prepareImage(root,{grant:'fixture-rebuild',tempRoot:root},fake);
assert.equal(rebuilt.inputDigest,cold.inputDigest);assert.equal(rebuilt.builds,1);assert.equal(builds,2);
assert.throws(()=>image.verifyImage(cold,root,()=>({Id:rebuilt.imageId,Os:'linux',Architecture:'amd64'})),/image ID/);
});
test('warm pinned execution records zero builds and checks the tag again after its final step',async t=>{
const {root,receipt}=imageFixture(t),out=join(root,'out');mkdirSync(out);let id=receipt.imageId;
const {executePinnedSteps}=await import('../../../scripts/testing/gate-lane.mjs');
const options={out,env:process.env,record:()=>{}},inspect=()=>({Id:id,Os:'linux',Architecture:'amd64'});
const result=await executePinnedSteps([{name:'warm',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}],options,{receipt,checkout:root,inspect});
assert.equal(result.rc,0);assert.equal(result.imageBuilds,0);
await assert.rejects(executePinnedSteps([{name:'last',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}],options,{receipt,checkout:root,inspect,afterStep:()=>id='sha256:'+'c'.repeat(64)}),/image ID/);
});
// These two read-only compatibility probes must reach the installed binary,
// including when the lane's guard is first on PATH. Child fixture env stays
// isolated; passing the real absolute path avoids re-entering that guard.
function installedDocker(){
const file=process.env.MOSAIC_GATE_DOCKER_RECORD;
if(!file)return 'docker';
const binary=JSON.parse(readFileSync(file,'utf8')).docker;
assert.ok(typeof binary==='string'&&isAbsolute(binary),'lane record must name the real absolute Docker binary');
return binary;
}
function composeFixture(t) {
const tempRoot=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(tempRoot,{recursive:true,force:true}));
const checkout=join(tempRoot,'candidate');mkdirSync(checkout);cpSync(new URL('../../../compose.yaml',import.meta.url),join(checkout,'compose.yaml'));
const config=join(tempRoot,'docker-config');mkdirSync(config,{mode:0o700});
const auth=join(tempRoot,'fixture-auth.json');writeFileSync(auth,'{}',{mode:0o600});
return {tempRoot,checkout,project:'mg197-a-abcdef123456',tag:'mosaic-poc-agent:fixture',imageId:'sha256:'+'a'.repeat(64),
env:{PATH:process.env.PATH,HOME:process.env.HOME,DOCKER_CONFIG:config,COMPOSE_PROJECT_NAME:'mg197-a-abcdef123456',
MOSAIC_IMAGE_TAG:'mosaic-poc-agent:fixture',MOSAIC_PROVIDER:'fixture',MOSAIC_MODEL:'fixture',MOSAIC_DATA_ROOT:tempRoot,PI_AUTH_FILE:auth}};
}
test('installed Compose accepts rewritten run argv without starting a container',t=>{
const run=composeFixture(t);
if(docker.writeComposeOverride)docker.writeComposeOverride(run);
const argv=docker.guardDocker(['compose','run','--rm','-T','mosaic-agent','fixture'],run,run.env);
const command=argv.indexOf('run');assert.ok(command>0);
// --help is before SERVICE, so parsing exits before any service operation.
const parsed=spawnSync(installedDocker(),[...argv.slice(0,command+1),'--help',...argv.slice(command+1)],{cwd:run.checkout,env:run.env,encoding:'utf8',timeout:10000});
assert.equal(parsed.status,0,parsed.stdout+parsed.stderr);
assert.match(parsed.stdout,/Usage:/);
});
test('installed Compose resolves the private override with no build and the pinned image',t=>{
const run=composeFixture(t);assert.equal(typeof docker.writeComposeOverride,'function');
const override=docker.writeComposeOverride(run);
assert.equal(override,join(run.tempRoot,'compose-no-build.yaml'));
assert.equal(statSync(override).mode&0o777,0o600);
assert.equal(docker.writeComposeOverride(run),override,'parallel callers reuse identical private content');
const argv=docker.guardDocker(['compose','run','--rm','mosaic-agent'],run,run.env),command=argv.indexOf('run');
const resolved=spawnSync(installedDocker(),[...argv.slice(0,command),'config','--format','json'],{cwd:run.checkout,env:run.env,encoding:'utf8',timeout:10000});
assert.equal(resolved.status,0,resolved.stdout+resolved.stderr);
const service=JSON.parse(resolved.stdout).services['mosaic-agent'];
assert.equal(Object.hasOwn(service,'build'),false);assert.equal(service.image,run.imageId);
t.diagnostic(JSON.stringify({installedComposeConfig:{buildPresent:Object.hasOwn(service,'build'),image:service.image}}));
assert.ok(argv.includes('--no-deps'));assert.equal(argv[argv.indexOf('--pull')+1],'never');
writeFileSync(override,'changed fixture');assert.throws(()=>docker.writeComposeOverride(run),/override/);
unlinkSync(override);const foreign=join(run.tempRoot,'foreign');writeFileSync(foreign,'fixture');symlinkSync(foreign,override);
assert.throws(()=>docker.writeComposeOverride(run),/override/);assert.equal(readFileSync(foreign,'utf8'),'fixture');
});
test('guard owns every Compose file argument and refuses caller configuration overrides',t=>{
const run=composeFixture(t);
for(const args of [['-f','foreign.yaml'],['--file','foreign.yaml'],['--file=foreign.yaml'],['--project-directory','/tmp'],['--build'],['--pull','always'],['--no-build']])
assert.throws(()=>docker.guardDocker(['compose','run',...args,'mosaic-agent'],run),/options not allowed/);
for(const args of [['compose','-f','foreign.yaml','run','mosaic-agent'],['compose','--project-directory','/tmp','run','mosaic-agent']])
assert.throws(()=>docker.guardDocker(args,run),/unowned command/);
});
+124
View File
@@ -0,0 +1,124 @@
import * as fsExtra from 'node:fs';
import * as childModule from 'node:child_process';
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const api=await import('../../../scripts/testing/lane-state.mjs');
function fixture(t){const root=mkdtempSync('/mnt/storage/scratch/gs-');t.after(()=>rmSync(root,{recursive:true,force:true}));return root;}
test('unfinished dead-owner runs quarantine their lane and a recovery record clears only that run',t=>{
assert.equal(typeof api.appendRecord,'function','appendRecord is required');
const root=fixture(t),dir=join(root,'runs','one');
api.appendRecord(dir,{kind:'start',lane:'a',owner:{pid:123456789,start:'1',boot:'fixture'}});
assert.equal(api.status(root,{processes:[]}).a.state,'QUARANTINED');
api.appendRecord(dir,{kind:'recovered',clean:true});
assert.equal(api.status(root,{processes:[]}).a.state,'CLEAR');
});
test('a legacy landing gate beside a lane stays legacy even with the same parent',t=>{
assert.equal(typeof api.appendRecord,'function');
const root=fixture(t);const owner={pid:123,start:'5',boot:'b',state:'S',ppid:1,argv:['node','gate-lane.mjs']};
api.appendRecord(join(root,'runs','one'),{kind:'start',lane:'a',owner});
const procs=[owner,{pid:124,start:'6',boot:'b',state:'S',ppid:1,argv:['node','/fixture/landing.mjs','gate','/fixture/wt','/fixture/logs']}];
const result=api.status(root,{processes:procs});assert.equal(result.a.state,'BUSY');assert.equal(result.b.state,'CLEAR');assert.deepEqual(result.legacy,[{pid:124,start:'6'}]);
});
test('the shell status entry reads scratch receipts and reports legacy processes without mutating old slots',t=>{
const root=fixture(t);const {spawnSync}=requireChild();
const r=spawnSync('bash',[new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname,'status','--state-root',root],{encoding:'utf8'});
assert.equal(r.status,0,r.stderr);const state=JSON.parse(r.stdout);assert.equal(state.a.state,'CLEAR');assert.equal(state.b.state,'CLEAR');assert.ok(Array.isArray(state.legacy));
});
function requireChild(){return childModule;}
test('recovery supersedes an unclean terminal without rewriting either receipt',t=>{
const root=fixture(t),dir=join(root,'runs','one');api.appendRecord(dir,{kind:'start',lane:'a',owner:{pid:99999999,start:'1',boot:'fixture'}});api.appendRecord(dir,{kind:'terminal',clean:false});api.appendRecord(dir,{kind:'recovered',clean:true});assert.equal(api.status(root,{processes:[]}).a.state,'CLEAR');assert.equal(api.records(dir).length,3);
});
test('the wrapper refuses a held lane lock and preserves its inode',async t=>{
const root=fixture(t),file=join(root,'a.lock');const {spawn,spawnSync}=childModule;
const holder=spawn('flock',['-x',file,'bash','-c','echo ready; read -r done'],{stdio:['pipe','pipe','pipe']});const exited=new Promise(r=>holder.once('exit',r));await new Promise(r=>holder.stdout.once('data',r));
const {statSync}=await import('node:fs');const ino=statSync(file).ino;
t.after(async()=>{holder.stdin.end('\n');await exited;});
const r=spawnSync('bash',[new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname,'run','--state-root',root,'--lane','a','--grant','fixture'],{encoding:'utf8'});
assert.equal(r.status,2);assert.match(r.stderr,/lock busy/);assert.equal(statSync(file).ino,ino);
});
test('a held kernel lock without a start receipt is BUSY rather than CLEAR',async t=>{
const root=fixture(t),file=join(root,'a.lock');const holder=childModule.spawn('flock',['-x',file,'bash','-c','echo ready; read -r done'],{stdio:['pipe','pipe','pipe']});const exited=new Promise(r=>holder.once('exit',r));await new Promise(r=>holder.stdout.once('data',r));t.after(async()=>{holder.stdin.end('\n');await exited;});
assert.equal(api.status(root,{processes:[]}).a.state,'BUSY');
});
test('SIGKILL leaves a quarantined run and locked recovery publishes a new clean receipt',async t=>{
const root=fixture(t),temp=mkdtempSync('/mnt/storage/scratch/gt-');t.after(()=>rmSync(temp,{recursive:true,force:true}));
const child=childModule.spawn(process.execPath,[new URL('./fixtures/gate-lane-child.mjs',import.meta.url).pathname,'wait',join(temp,'started')],{stdio:['ignore','pipe','pipe']});
const exited=new Promise(r=>child.once('exit',r));await new Promise(r=>child.stdout.once('data',r));
const id='abcdef123456',dir=join(root,'runs',id),docker=join(root,'docker-fixture');writeFileSync(docker,'#!/bin/sh\nexit 0\n',{mode:0o700});
api.appendRecord(dir,{kind:'start',lane:'a',owner:api.identity(child.pid),tempRoot:temp,project:`mg197-a-${id}`,docker});
child.kill('SIGKILL');await exited;assert.equal(api.status(root,{processes:[]}).a.state,'QUARANTINED');
const r=childModule.spawnSync('bash',[new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname,'recover','--state-root',root,'--run-id',id,'--grant','fixture-recover'],{encoding:'utf8'});
assert.equal(r.status,0,r.stderr);assert.equal(api.status(root,{processes:[]}).a.state,'CLEAR');assert.equal(api.records(dir).filter(e=>e.kind==='start').length,1);assert.equal(api.records(dir).filter(e=>e.kind==='recovered').length,1);
});
test('a malformed receipt refuses status and an identity mismatch cannot look live',t=>{
const root=fixture(t),dir=join(root,'runs','one');api.appendRecord(dir,{kind:'start',lane:'a',owner:{pid:123,start:'old',boot:'b'}});
assert.equal(api.status(root,{processes:[{pid:123,start:'new',boot:'b',state:'S',ppid:1,argv:['node','owned'] }]}).a.state,'QUARANTINED');
writeFileSync(join(dir,'bad.json'),'{');assert.throws(()=>api.status(root,{processes:[]}),/invalid run receipt/);
});
test('a symlink lock is refused before acquiring or replacing its target',t=>{
const root=fixture(t),target=join(root,'foreign');writeFileSync(target,'untouched');const {symlinkSync}=fsExtra;symlinkSync(target,join(root,'a.lock'));
const r=childModule.spawnSync('bash',[new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname,'run','--state-root',root,'--lane','a','--grant','fixture'],{encoding:'utf8'});
assert.equal(r.status,2);assert.match(r.stderr,/ordinary file/);assert.equal(readFileSync(target,'utf8'),'untouched');
});
test('failed recovery keeps quarantine and never publishes a clean recovery receipt',async t=>{
const root=fixture(t),temp=mkdtempSync('/mnt/storage/scratch/gt-');t.after(()=>rmSync(temp,{recursive:true,force:true}));
const id='abcdef123456',dir=join(root,'runs',id),docker=join(root,'docker-fixture');writeFileSync(docker,'#!/bin/sh\nexit 1\n',{mode:0o700});
api.appendRecord(dir,{kind:'start',lane:'a',owner:{pid:99999999,start:'1',boot:'fixture'},tempRoot:temp,project:`mg197-a-${id}`,docker});
const r=childModule.spawnSync('bash',[new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname,'recover','--state-root',root,'--run-id',id,'--grant','fixture-recover'],{encoding:'utf8'});
assert.equal(r.status,2);assert.equal(api.status(root,{processes:[]}).a.state,'QUARANTINED');assert.equal(api.records(dir).some(e=>e.kind==='recovered'),false);
});
test('two shared image holders coexist, but exclusive preparation refuses their lock',async t=>{
const root=fixture(t),file=join(root,'image.lock');const holder=childModule.spawn('flock',['-s',file,'bash','-c','echo ready; read -r done'],{stdio:['pipe','pipe','pipe']});const exited=new Promise(r=>holder.once('exit',r));await new Promise(r=>holder.stdout.once('data',r));t.after(async()=>{holder.stdin.end('\n');await exited;});
const entry=new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname;
const shared=childModule.spawnSync('bash',[entry,'run','--state-root',root,'--lane','a','--grant','fixture'],{encoding:'utf8'});
assert.equal(shared.status,2);assert.doesNotMatch(shared.stderr,/lock busy|descriptor|not locked/);
const exclusive=childModule.spawnSync('bash',[entry,'prepare-image','--state-root',root,'--grant','fixture'],{encoding:'utf8'});
assert.equal(exclusive.status,2);assert.match(exclusive.stderr,/lock busy/);
});
const entry=new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname;
const childFixture=new URL('./fixtures/gate-lane-child.mjs',import.meta.url).pathname;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function until(check){for(let n=0;n<100;n++){if(check())return;await sleep(20);}throw Error('fixture barrier did not become ready');}
test('two simultaneous shell claims have one winner; supervisor retains lock FDs and ordinary children do not',async t=>{
const root=fixture(t),bin=join(root,'bin');fsExtra.mkdirSync(bin);const shim=join(bin,'node');
writeFileSync(shim,`#!/bin/sh\nif [ "$2" = --locked ]; then exec '${process.execPath}' '${childFixture}' lock-probe '${root}'; fi\nexec '${process.execPath}' "$@"\n`,{mode:0o700});
const env={...process.env,PATH:bin+':'+process.env.PATH};
const start=()=>{const p=childModule.spawn('bash',[entry,'run','--state-root',root,'--lane','a','--grant','fixture'],{env,stdio:['ignore','pipe','pipe']});p.done=new Promise(r=>p.once('exit',code=>r(code)));return p;};
const first=start(),second=start();t.after(()=>{first.kill('SIGKILL');second.kill('SIGKILL');});
await until(()=>fsExtra.existsSync(join(root,'ready')));
const all=api.runRecords(root);assert.equal(all.length,1);assert.equal(readFileSync(join(root,'out/fds.log'),'utf8'),'');
const winner=all[0].records.find(r=>r.kind==='start').owner.pid;assert.ok([first.pid,second.pid].includes(winner));
const loser=first.pid===winner?second:first;assert.equal(await loser.done,2);
const ino=fsExtra.statSync(join(root,'a.lock')).ino;
assert.equal(childModule.spawnSync('flock',['-n',join(root,'a.lock'),'true']).status,1);
const won=first.pid===winner?first:second;won.kill('SIGTERM');await won.done;
assert.equal(fsExtra.statSync(join(root,'a.lock')).ino,ino);
});
test('same checkout and a legacy land gate each refuse a claim on an otherwise free lane',async t=>{
const {activeConflicts}=await import('../../../scripts/testing/gate-lane.mjs');
const root=fixture(t),owner={pid:321,start:'1',boot:'test',state:'S',ppid:1,argv:['node','owned']};
api.appendRecord(join(root,'runs','one'),{kind:'start',lane:'a',checkout:'/fixture/candidate',owner});
const o={stateRoot:root,lane:'b',checkout:'/fixture/candidate'};
assert.throws(()=>activeConflicts(o,{processes:[owner]}),/checkout already owned/);
assert.throws(()=>activeConflicts({...o,checkout:'/fixture/other'},{processes:[owner,{pid:322,start:'2',boot:'test',state:'S',ppid:1,argv:['node','/fixture/landing.mjs','gate']}]}),/LEGACY BUSY/);
});
test('recovery kills a proven surviving child, preserves a foreign child and keeps quarantine if interrupted',async t=>{
const root=fixture(t),temp=mkdtempSync('/mnt/storage/scratch/gt-');t.after(()=>rmSync(temp,{recursive:true,force:true}));
const owned=childModule.spawn(process.execPath,[childFixture,'wait',join(temp,'child')],{cwd:temp,stdio:'ignore'});
const foreign=childModule.spawn(process.execPath,[childFixture,'wait',join(root,'foreign')],{stdio:'ignore'});
const ownedDone=new Promise(r=>owned.once('exit',r)),foreignDone=new Promise(r=>foreign.once('exit',r));t.after(async()=>{owned.kill('SIGKILL');foreign.kill('SIGKILL');await Promise.all([ownedDone,foreignDone]);});
await until(()=>fsExtra.existsSync(join(temp,'child'))&&fsExtra.existsSync(join(root,'foreign')));
const docker=join(root,'docker'),pidFile=join(root,'docker-pid'),id='abcdef123456',dir=join(root,'runs',id);
writeFileSync(docker,`#!/bin/sh\necho $$ > '${pidFile}'\nexec sleep 60\n`,{mode:0o700});
api.appendRecord(dir,{kind:'start',id,stateRoot:root,lane:'a',owner:{pid:99999999,start:'1',boot:'test'},tempRoot:temp,project:`mg197-a-${id}`,docker});
const recovery=childModule.spawn('bash',[entry,'recover','--state-root',root,'--run-id',id,'--grant','fixture'],{stdio:'ignore'});const done=new Promise(r=>recovery.once('exit',r));
await until(()=>fsExtra.existsSync(pidFile));const dockerOwner=api.identity(Number(readFileSync(pidFile,'utf8')));recovery.kill('SIGKILL');await done;
if(api.sameProcess(dockerOwner))process.kill(dockerOwner.pid,'SIGKILL');
await ownedDone;assert.equal(api.sameProcess(api.identity(foreign.pid)),true);
assert.equal(api.records(dir).some(r=>r.kind==='recovered'),false);assert.equal(api.status(root,{processes:[]}).a.state,'QUARANTINED');
writeFileSync(docker,'#!/bin/sh\nexit 0\n',{mode:0o700});
const retried=childModule.spawnSync('bash',[entry,'recover','--state-root',root,'--run-id',id,'--grant','fixture-resume'],{encoding:'utf8'});
assert.equal(retried.status,0,retried.stderr);assert.equal(api.status(root,{processes:[]}).a.state,'CLEAR');assert.equal(api.sameProcess(api.identity(foreign.pid)),true);
});
+125
View File
@@ -0,0 +1,125 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import { join } from 'node:path';
const api = await import('../../../scripts/testing/gate-lane.mjs').catch(e => {
if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e;
return {};
});
function fixture(t) {
const root = mkdtempSync('/mnt/storage/scratch/gt-');
// Node appends six characters, keeping this root at 30 bytes.
t.after(() => rmSync(root, { recursive: true, force: true }));
const checkout = join(root, 'checkout'); mkdirSync(checkout);
for (const [f,text] of [['package.json','{}'],['RELEASE','0.0.1\n']]) writeFileSync(join(checkout,f),text);
execFileSync('git',['init','-q',checkout]);
execFileSync('git',['-C',checkout,'add','.']);
execFileSync('git',['-C',checkout,'-c','user.name=Fixture','-c','user.email=f@invalid','commit','-qm','fixture']);
symlinkSync('/mnt/storage/src/mosaic-stack/node_modules',join(checkout,'node_modules'));
return {lane:'a',candidate:execFileSync('git',['-C',checkout,'rev-parse','HEAD'],{encoding:'utf8'}).trim(),checkout,out:join(root,'out'),tempRoot:root,stateRoot:join(root,'state'),grant:'fixture-grant',hostFacts:{unprivilegedPortStart:1024,ephemeral:[32768,60999],listeners:[]}};
}
test('preflight refuses unsafe lane roots, missing grants and live port 9 before side effects', t => {
assert.equal(typeof api.validateRun,'function','validateRun is required');
const f=fixture(t);
assert.equal(api.validateRun(f).lane,'a');
for(const [patch,reason] of [[{lane:'c'},/lane/],[{grant:''},/grant/],[{tempRoot:'/tmp'},/tmp/],[{tempRoot:join(f.tempRoot,'deeper')},/30|missing/],[{candidate:'a'.repeat(40)},/candidate/],[{hostFacts:{...f.hostFacts,listeners:[{address:'0.0.0.0',port:9}]}},/port.9/],[{hostFacts:{...f.hostFacts,unprivilegedPortStart:0}},/privileged/]]) assert.throws(()=>api.validateRun({...f,...patch}),reason);
const link=join(f.tempRoot,'link');symlinkSync('/tmp',link);assert.throws(()=>api.validateRun({...f,tempRoot:link}),/tmp/);
writeFileSync(join(f.checkout,'RELEASE'),'changed');assert.throws(()=>api.validateRun(f),/dirty/);
});
test('discovery preserves all package and script steps while ignoring non-file shell names', t => {
const f=fixture(t);mkdirSync(join(f.checkout,'packages'));
for(const p of ['z','queue','a']){mkdirSync(join(f.checkout,'packages',p,'tests'),{recursive:true});writeFileSync(join(f.checkout,'packages',p,'tests','one.test.mjs'),'');}
mkdirSync(join(f.checkout,'scripts'));writeFileSync(join(f.checkout,'scripts','test-z.sh'),'');writeFileSync(join(f.checkout,'scripts','test-a.sh'),'');mkdirSync(join(f.checkout,'scripts','test-directory.sh'));
const steps=api.discoverSteps(f.checkout);
assert.deepEqual(steps.map(s=>s.name),['pkg-a','pkg-z','script-a','script-z']);
assert.deepEqual(steps[0].argv,['node','--test','--test-concurrency=2','tests/one.test.mjs']);
});
test('a failed step stops the gate and records its nonzero exit without running the next step',async t=>{
assert.equal(typeof api.executeSteps,'function','executeSteps is required');
const f=fixture(t),events=[],out=f.out;mkdirSync(out);const marker=join(f.tempRoot,'should-not-exist');
const result=await api.executeSteps([{name:'first',cwd:f.checkout,argv:['bash','-c','exit 7']},{name:'second',cwd:f.checkout,argv:['touch',marker]}],{out,env:process.env,record:r=>events.push(r)});
assert.equal(result.rc,7);assert.equal(events.filter(e=>e.kind==='step').length,1);assert.ok(events.find(e=>e.kind==='step').wallMs>=0);
const {existsSync}=await import('node:fs');assert.equal(existsSync(marker),false);
});
test('a cancelled step stops its process group and records failure',async t=>{
const f=fixture(t);mkdirSync(f.out);const controller=new AbortController(),events=[];
const child=new URL('./fixtures/gate-lane-child.mjs',import.meta.url).pathname;
const result=await api.executeSteps([{name:'child',cwd:f.checkout,argv:[process.execPath,child,'wait',join(f.tempRoot,'started')]}],{out:f.out,env:process.env,record:e=>{events.push(e);if(e.kind==='child')setTimeout(()=>controller.abort(),100);},signal:controller.signal});
assert.notEqual(result.rc,0);const owned=events.find(e=>e.kind==='child').owner;const {sameProcess}=await import('../../../scripts/testing/lane-state.mjs');assert.equal(sameProcess(owned),false);
});
test('step receipts retain pass fail skipped counts and hashes of their actual logs',async t=>{
const f=fixture(t);mkdirSync(f.out);const events=[];
await api.executeSteps([{name:'counts',cwd:f.checkout,argv:[process.execPath,'-e',"console.log('ℹ pass 2\\nℹ fail 0\\nℹ skipped 1')"]}],{out:f.out,env:process.env,record:r=>events.push(r)});
const step=events.find(e=>e.kind==='step');assert.deepEqual(step.counts,{pass:2,fail:0,skipped:1});assert.match(step.logSha256,/^[a-f0-9]{64}$/);
});
test('preflight rejects a real 31-byte root, missing dependency tree, occupied band and missing inventory',t=>{
const f=fixture(t),long=f.tempRoot+'x';mkdirSync(long,{mode:0o700});t.after(()=>rmSync(long,{recursive:true,force:true}));assert.equal(Buffer.byteLength(long),31);
assert.throws(()=>api.validateRun({...f,tempRoot:long}),/30 bytes/);
assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,ephemeral:[60000,62000]}}),/overlaps/);
assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,listeners:[{address:'127.0.0.1',port:61005}]}}),/live listener/);
assert.throws(()=>api.validateRun({...f,hostFacts:{}}),/inventory/);
rmSync(join(f.checkout,'node_modules'));assert.throws(()=>api.validateRun(f),/node_modules/);
});
test('preflight refuses canonical and builder checkouts and an existing output',t=>{
const f=fixture(t);
for(const checkout of ['/mnt/storage/src/mosaic-stack',new URL('../../../',import.meta.url).pathname])assert.throws(()=>api.validateRun({...f,checkout}),/canonical or builder/);
mkdirSync(f.out);assert.throws(()=>api.validateRun(f),/output already exists/);
});
test('both candidates must carry every approved launcher file',t=>{
const f=fixture(t),other=join(f.tempRoot,'second');mkdirSync(other);
for(const checkout of [f.checkout,other]){
const target=join(checkout,'scripts','testing');fsExtra.cpSync(new URL('../../../scripts/testing',import.meta.url),target,{recursive:true});
api.validateTooling(checkout);
}
fsExtra.appendFileSync(join(other,'scripts/testing/lane-state.mjs'),'\n// changed\n');
assert.throws(()=>api.validateTooling(other),/tooling differs/);
fsExtra.rmSync(join(f.checkout,'scripts/testing/lane-fixtures.mjs'));
assert.throws(()=>api.validateTooling(f.checkout),/tooling differs/);
});
test('the 92-byte socket example passes and an over-107-byte pathname refuses',()=>{
const root='/mnt/storage/scratch/gt-ABCDEF';assert.equal(Buffer.byteLength(root),30);
const socket=root+'/mosaic-cli-ABCDEF/data/launches/acme/r0123456789ab/tools.sock';
assert.equal(Buffer.byteLength(socket),92);assert.equal(stateApi.socketPath(socket),socket);
assert.throws(()=>stateApi.socketPath(socket+'x'.repeat(16)),/107 bytes/);
});
test('package/script duplicates stay and every supervised step clears NODE_TEST_CONTEXT',async t=>{
const f=fixture(t);mkdirSync(join(f.checkout,'packages/cli/tests'),{recursive:true});mkdirSync(join(f.checkout,'scripts'));
writeFileSync(join(f.checkout,'packages/cli/tests/repeated.test.mjs'),'');writeFileSync(join(f.checkout,'scripts/test-cli.sh'),'node --test packages/cli/tests/repeated.test.mjs\n');
const steps=api.discoverSteps(f.checkout);assert.deepEqual(steps.map(s=>s.name),['pkg-cli','script-cli']);
const original=process.env.NODE_TEST_CONTEXT;process.env.NODE_TEST_CONTEXT='parent-context';
let env;try{env=api.cleanEnv(f,{project:'fixture'},'/fake/docker');}finally{if(original===undefined)delete process.env.NODE_TEST_CONTEXT;else process.env.NODE_TEST_CONTEXT=original;}
assert.equal(env.NODE_TEST_CONTEXT,undefined);assert.equal(env.HOME,process.env.HOME);
mkdirSync(f.out);const events=[];
for(const s of steps)s.argv=[process.execPath,'-e',"if(process.env.NODE_TEST_CONTEXT)process.exit(9);console.log('cleared')"];
const result=await api.executeSteps(steps,{out:f.out,env,record:e=>events.push(e)});
assert.equal(result.rc,0);assert.equal(events.filter(e=>e.kind==='step').length,2);
});
test('scoped ss loopback and wildcard port 9 listeners refuse',t=>{
const f=fixture(t);
for(const address of ['127.0.0.1%lo','0.0.0.0%lo','::%lo','::ffff:127.0.0.1%lo'])assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,listeners:[{address,port:9}]}}),/port 9/);
});
import * as fsExtra from 'node:fs';
import * as stateApi from '../../../scripts/testing/lane-state.mjs';
test('step children use legacy umask 022 while the supervisor retains 077', async t => {
const f=fixture(t);mkdirSync(f.out,{mode:0o700});const events=[],made=join(f.tempRoot,'step-file');
const original=process.umask(0o077);
try {
const result=await api.executeSteps([{name:'umask',cwd:f.checkout,argv:[process.execPath,'-e',`const fs=require('node:fs');console.log(process.umask().toString(8));fs.writeFileSync(${JSON.stringify(made)},'fixture',{mode:0o666});`]}],{out:f.out,env:process.env,record:r=>events.push(r)});
assert.equal(result.rc,0);assert.equal(process.umask(),0o077);
assert.equal(fsExtra.readFileSync(join(f.out,'umask.log'),'utf8').trim(),'22');
assert.equal(fsExtra.statSync(made).mode&0o777,0o644);
assert.equal(fsExtra.statSync(join(f.out,'umask.log')).mode&0o777,0o600);
} finally {process.umask(original);}
});
test('lane wrapper keeps its state directory and lock files private', t => {
const f=fixture(t),script=new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname;
const result=spawnSync('bash',[script,'recover','--state-root',f.stateRoot,'--run-id','000000000000','--grant','fixture-private-modes'],{encoding:'utf8'});
assert.equal(result.status,2,result.stdout+result.stderr);assert.match(result.stderr,/run not found/);
assert.equal(fsExtra.statSync(f.stateRoot).mode&0o777,0o700);
for(const name of ['a.lock','b.lock','image.lock'])assert.equal(fsExtra.statSync(join(f.stateRoot,name)).mode&0o777,0o600);
});
+3 -1
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
// scripts/stack-release.sh (D1, #1659). Design, the T table and the mutants:
// agents/darkwing/work/queue-162/BUILD.md. Every run uses a temp data root, temp units and stand-in
// systemctl, journalctl and systemd-analyze on PATH; nothing here reads ~/.mosaic-dev or a live unit.
@@ -1784,7 +1786,7 @@ for(const [id,v,froms] of [[B,'3g',['3f']],[C,'3h',['3f','3g']],[A,'3g',['3f']],
}
const TESTER='7499';
const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
const rh=(e,...p)=>join(e.env.HOME,...p);
const rfault=(e,name)=>{mkdirSync(rh(e,'faults'),{recursive:true});writeFileSync(rh(e,'faults',name),'')};
+3 -10
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
import { test, after } from "node:test";
import assert from "node:assert/strict";
import {
@@ -73,16 +75,7 @@ function closeServer(server) {
// Grab an ephemeral free port, then hand it back immediately so a caller can
// try to bind it themselves (used to prove startServer never opened a socket).
function getFreePort() {
return new Promise((resolvePromise, reject) => {
const probe = createNetServer();
probe.once("error", reject);
probe.listen(0, "127.0.0.1", () => {
const port = probe.address().port;
probe.close(() => resolvePromise(port));
});
});
}
function getFreePort() { return leasePort({after:laneAfter}, {closed:true}); }
function runCli(args) {
return spawnSync(process.execPath, [cli, ...args], { encoding: "utf8", timeout: 15000 });
@@ -1,3 +1,4 @@
import { fixtureCacheEnv } from '../../../scripts/testing/lane-fixtures.mjs';
// The host's claude CLI through adapters/claude with the bundle's hook and
// MCP server, against the scripted Messages API. Scratch CLAUDE_CONFIG_DIR
// and HOME, a dummy key, nonessential traffic off: nothing reaches a real
@@ -61,7 +62,7 @@ async function session(t, script, tools = ["read", "bash"]) {
function turn(env, request, cwd, adapter = ADAPTER) {
return new Promise((resolve) => {
const child = spawn("/bin/sh", [adapter], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true });
const child = spawn("/bin/sh", [adapter], { cwd, env: { ...env, ...fixtureCacheEnv(env.CLAUDE_CONFIG_DIR), MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true });
let stdout = "";
let stderr = "";
child.stdout.on("data", (b) => (stdout += b));
+2 -1
View File
@@ -1,3 +1,4 @@
import { fixtureCacheEnv } from '../../../scripts/testing/lane-fixtures.mjs';
// Real pi (the pinned CLI) through adapters/pi with the extension, against
// the scripted Messages API. No real model, no network beyond 127.0.0.1.
@@ -57,7 +58,7 @@ async function session(t, script, place = (dir) => join(dir, "ws"), instance = "
function turn(env, request, cwd) {
return new Promise((resolve) => {
const child = spawn("/bin/sh", [ADAPTER], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"] });
const child = spawn("/bin/sh", [ADAPTER], { cwd, env: { ...env, ...fixtureCacheEnv(env.PI_CODING_AGENT_DIR), MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.on("data", (b) => (stdout += b));
+3 -2
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
// Row 74 (#1568, lead decision 93): the preview mode. A request for the public name needs the
// proxy's header equal to the one user id, else 401 with nothing read; in preview mode nothing
// but GET and HEAD gets through, whatever the Host; loopback requests work as they did. Round 2:
@@ -205,8 +207,7 @@ test('in preview mode every write answers 403 read-only, from the public name an
});
test('loopback requests are unchanged in preview mode, and the bus views work with the board down', async t => {
const probe = createServer(); await new Promise(r => probe.listen(0, '127.0.0.1', r));
const dead = `http://127.0.0.1:${probe.address().port}`; await close(probe);
const dead = `http://127.0.0.1:${await leasePort({after:laneAfter}, {closed:true})}`;
const { port, busCalls } = await setup(t, { board: dead, timeout: 200 });
const loopback = { host: `127.0.0.1:${port}` };
const inbox = await send(port, { headers: loopback });
+3 -2
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
@@ -62,8 +64,7 @@ test('serve argv carries only fixture proof path and header; startup explicitly
test('CLI applies proof on the main listener and rejects that header on its tester listener', async t => {
const f = await testerFixture(t), file = join(f.root, 'proof.fixture');
writeFileSync(file, VALUE, { mode: 0o600 });
const free = createServer(); free.listen(0, '127.0.0.1'); await once(free, 'listening');
const testerPort = free.address().port; await new Promise(r => free.close(r));
const testerPort = await leasePort({after:laneAfter}, {closed:true});
const child = spawn(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', '--tester-port', String(testerPort), '--business', 'acme', ...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file], { env: f.f.env, stdio: ['ignore', 'pipe', 'pipe'] });
let output = ''; child.stdout.on('data', c => output += c); child.stderr.resume();
t.after(async () => { if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await once(child, 'close'); } });
+3 -2
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createServer, get } from 'node:http';
@@ -97,8 +99,7 @@ test('proxy preserves exact request bytes, status and receipt, rejects forms and
});
test('unreachable board reports URL; CLI rejects unsupported options', async () => {
const probe = createServer(); await new Promise(r => probe.listen(0, '127.0.0.1', r));
const url = `http://127.0.0.1:${probe.address().port}`; await close(probe);
const url = `http://127.0.0.1:${await leasePort({after:laneAfter}, {closed:true})}`;
const web = await startServer({ port: 0, board: url, timeout: 100 });
try {
const res = await fetch(`http://127.0.0.1:${web.address().port}/api/board`);
+3 -9
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
@@ -45,15 +47,7 @@ function filled(flags, root = REPO) {
// A loopback port with nothing listening, so status's /healthz request
// reaches no real Console.
async function closedPort() {
const server = createServer();
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const { port } = server.address();
server.close();
await once(server, 'close');
return port;
}
async function closedPort() { return leasePort({after:laneAfter}, {closed:true}); }
function tmp(t) {
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
+3 -4
View File
@@ -1,3 +1,5 @@
import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
import { after as laneAfter } from 'node:test';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:net';
@@ -15,10 +17,7 @@ const get = (port, path) => new Promise((resolve, reject) => {
let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, body: text }));
}); q.on('error', reject); q.end();
});
async function freePort() {
const s = createServer(); s.listen(0, '127.0.0.1'); await once(s, 'listening');
const port = s.address().port; await new Promise(r => s.close(r)); return port;
}
async function freePort() { return leasePort({after:laneAfter}, {closed:true}); }
test('tester-port validates range tunnel main port and requires configured bus before listening', t => {
const root = tmp(t, 'tester-cli-invalid-'), f = fixture(root);
+10 -1
View File
@@ -71,6 +71,8 @@ EOF_PROBE
mkdir -p "$SANDBOX/toolroot/plans"
echo "row one" >"$SANDBOX/toolroot/plans/QUEUE.md"
TOOLS_JSON="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
export PI_CODING_AGENT_DIR="$SANDBOX/pi-agent"
mkdir -p "$PI_CODING_AGENT_DIR"
PI_COMMON="--mode rpc --no-extensions --no-context-files --no-skills --no-prompt-templates --no-themes --offline --no-session --provider zai --model glm-5.3"
if [ -x "$PI_BIN" ]; then
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_JSON" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
@@ -142,7 +144,14 @@ fi
# A nested `node --test` inherits a parent runner's NODE_TEST_CONTEXT, reports
# to that runner and exits 0 whatever its tests do, so the suite clears it
# (#1508 N13). The planted failing test proves a failure still fails here.
node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
node_tests() {
local lane_concurrency=()
if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
[ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || return 2
lane_concurrency=(--test-concurrency=2)
fi
env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" "$@"
}
mkdir -p "$SANDBOX/planted"
printf '%s\n' 'import { test } from "node:test";' 'import assert from "node:assert/strict";' 'test("planted failure", () => assert.equal(1, 2));' >"$SANDBOX/planted/planted.test.mjs"
NODE_TEST_CONTEXT=child-v8 node_tests "$SANDBOX/planted/" >"$SANDBOX/planted.log" 2>&1
+8 -1
View File
@@ -76,7 +76,14 @@ check "checked-in demo bundles equal a fresh generation" $DEMO_OK
# A nested `node --test` inherits a parent runner's NODE_TEST_CONTEXT, reports
# to that runner and exits 0 whatever its tests do, so the suite clears it
# (#1508 N13). The planted failing test proves a failure still fails here.
node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
node_tests() {
local lane_concurrency=()
if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
[ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || return 2
lane_concurrency=(--test-concurrency=2)
fi
env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" "$@"
}
mkdir -p "$SANDBOX/planted"
printf '%s\n' 'import { test } from "node:test";' 'import assert from "node:assert/strict";' 'test("planted failure", () => assert.equal(1, 2));' >"$SANDBOX/planted/planted.test.mjs"
NODE_TEST_CONTEXT=child-v8 node_tests "$SANDBOX/planted/" >"$SANDBOX/planted.log" 2>&1
+6 -1
View File
@@ -47,7 +47,12 @@ node -e 'const p=require("./packages/queue/package.json"); process.exit(p.depend
check "packages/queue declares no dependencies" $?
# --- the package tests ---
env -u NODE_TEST_CONTEXT node --test --test-reporter=spec packages/queue/tests/ > "$SANDBOX/tests.log" 2>&1
lane_concurrency=()
if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
[ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || exit 2
lane_concurrency=(--test-concurrency=2)
fi
env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" --test-reporter=spec packages/queue/tests/ > "$SANDBOX/tests.log" 2>&1
rc=$?
grep -E '^ℹ (tests|pass|fail) ' "$SANDBOX/tests.log"
if [ "$rc" != 0 ]; then grep -E '^✖|Error' "$SANDBOX/tests.log" | head -40; fi
+13 -4
View File
@@ -13,6 +13,11 @@ cp RELEASE "$RELEASE_BACKUP"
# One exit trap: the repo RELEASE is ALWAYS restored from the backup,
# regardless of how the test run ends.
trap 'cp "$RELEASE_BACKUP" RELEASE 2>/dev/null; rm -rf "$SANDBOX" "$RELEASE_BACKUP"' EXIT
# Compose mounts PI_AUTH_FILE. Point it at a fixture so no activation reaches
# the host's pi credential (row 244). chmod after the write: umask-proof (row 243).
{ mkdir -p "$SANDBOX/pi" && printf '{}' > "$SANDBOX/pi/auth.json" && chmod 600 "$SANDBOX/pi/auth.json"; } \
|| { echo "cannot write the pi auth fixture" >&2; exit 1; }
export PI_AUTH_FILE="$SANDBOX/pi/auth.json"
PASS=0
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
@@ -57,8 +62,11 @@ check "valid RELEASE leaves image tag consistent with version" $?
# ---------- sandboxed state machine (Docker required) ----------
if docker info >/dev/null 2>&1; then
mkdir -p "$SANDBOX/data"
# The mock adapter: no credential and no provider call in the gate (row 244,
# ruling A, sage-1914). This suite proves the release state machine, image and
# Compose path, not that the pi image reaches the provider.
cat > "$SANDBOX/config.json" <<EOF
{"configVersion":1,"environment":"development","dataRoot":"$SANDBOX/data","execution":{"backend":"docker","provider":"zai","model":"glm-5.3-flash"}}
{"configVersion":1,"environment":"development","dataRoot":"$SANDBOX/data","execution":{"backend":"docker","provider":"zai","model":"glm-5.3-flash","adapter":"mock"}}
EOF
export MOSAIC_CONFIG="$SANDBOX/config.json"
@@ -66,7 +74,8 @@ EOF
[ ! -e "$SANDBOX/data/state/active.json" ] \
&& check "status created no pointer" 0 || check "status created no pointer" 1
expect_exit "fault-injected activation refuses" 1 -- scripts/release.sh activate --fault-injection
# The mock gives the right marker, so only the fault's wrong expectation refuses.
expect_exit "fault-injected activation refuses" 1 -- env MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK scripts/release.sh activate --fault-injection
[ ! -e "$SANDBOX/data/state/active.json" ] \
&& check "refused activation wrote no pointer" 0 || check "refused activation wrote no pointer" 1
if [ -f "$SANDBOX/data/state/activation-log.jsonl" ]; then
@@ -82,7 +91,7 @@ process.exit(e.event === "refused" && e.release && e.imageTag && e.at ? 0 : 1);
check "refusal logged exactly once with valid fields" 1
fi
expect_exit "healthy activation succeeds" 0 -- scripts/release.sh activate
expect_exit "healthy activation succeeds" 0 -- env MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK scripts/release.sh activate
node -e '
const fs = require("fs");
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
@@ -90,7 +99,7 @@ process.exit(p.pointerVersion === 1 && p.release && p.imageTag && p.activatedAt
' "$SANDBOX/data/state/active.json"
check "pointer written with valid fields" $?
expect_exit "repeat activation succeeds (log grows)" 0 -- scripts/release.sh activate
expect_exit "repeat activation succeeds (log grows)" 0 -- env MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK scripts/release.sh activate
LINES=$(grep -c '' "$SANDBOX/data/state/activation-log.jsonl")
[ "$LINES" -ge 3 ] && check "log is append-only across activations" 0 || check "log is append-only across activations" 1
+34 -9
View File
@@ -9,6 +9,18 @@ cd "$(dirname "$0")/.."
SANDBOX="$(mktemp -d)"
trap 'rm -rf "$SANDBOX"' EXIT
# Real-model cases need the operator's credential and a billed provider call,
# so they run only on request (row 244, sage-2061). The gate lane never passes
# MOSAIC_LIVE_PROVIDER through.
LIVE_SKIP="skip live provider cases (row 244: no provider call in the gate)"
# Compose mounts PI_AUTH_FILE. Unless live cases were requested, point it at a
# fixture so no run reaches the host's pi credential (row 244). chmod after the
# write: umask-proof (row 243).
if [ "${MOSAIC_LIVE_PROVIDER:-}" != "1" ]; then
{ mkdir -p "$SANDBOX/pi" && printf '{}' > "$SANDBOX/pi/auth.json" && chmod 600 "$SANDBOX/pi/auth.json"; } \
|| { echo "cannot write the pi auth fixture" >&2; exit 1; }
export PI_AUTH_FILE="$SANDBOX/pi/auth.json"
fi
PASS=0
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
@@ -423,7 +435,8 @@ else
echo "skip workspace/capability cases (docker daemon unavailable)"
fi
# ---------- live: real runs (Docker + credentials required) ----------
# ---------- run records through the mock adapter (Docker required) ----------
# These prove the run-record path, not the model, so the mock answers (row 244).
# On failure, surface the run record + agent stderr BEFORE the sandbox
# cleanup destroys them. Never let a wrong-exit mask the real reason.
dump_latest_run() {
@@ -441,14 +454,15 @@ dump_latest_run() {
if docker info >/dev/null 2>&1; then
RUNS1=$(ls "$DATA_ROOT/runs" 2>/dev/null | wc -l)
if scripts/run-task.sh run "$SANDBOX/ok.json" >/dev/null 2>&1; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} live hello task succeeds with exact marker"
if env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK \
scripts/run-task.sh run "$SANDBOX/ok.json" >/dev/null 2>&1; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} hello task succeeds with exact marker"
else
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} live hello task succeeds with exact marker" >&2
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} hello task succeeds with exact marker" >&2
dump_latest_run
fi
R1="$(ls -t "$DATA_ROOT/runs" | head -1)" # newest = the live run above
R1="$(ls -t "$DATA_ROOT/runs" | head -1)" # newest = the hello run above
[ -f "$DATA_ROOT/runs/$R1/result.json" ] && check "result.json written in run dir" 0 || check "result.json written in run dir" 1
node -e '
const r = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
@@ -457,7 +471,8 @@ process.exit(r.status === "succeeded" && r.response === "MOSAIC_HELLO_OK" && r.e
check "result.json contents are correct" $?
printf '{"taskVersion":1,"id":"t-wrong","prompt":"Return your startup marker and nothing else.","expectExact":"MOSAIC_NOT_OK"}' > "$SANDBOX/wrong.json"
scripts/run-task.sh run "$SANDBOX/wrong.json" >/dev/null 2>&1
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK \
scripts/run-task.sh run "$SANDBOX/wrong.json" >/dev/null 2>&1
RC=$?
WRONG_REASON="$(latest_reason)"
if [ "$RC" -eq 1 ] && [ "$WRONG_REASON" = "expect-mismatch" ]; then
@@ -473,6 +488,14 @@ process.exit(r.status === "succeeded" && r.response === "MOSAIC_HELLO_OK" && r.e
COUNT=$($TASK list | wc -l)
[ "$COUNT" -ge 2 ] && check "list shows both runs" 0 || check "list shows both runs" 1
else
echo "skip run-record cases (docker unavailable)"
fi
# ---------- live: real model (Docker + operator credential, on request) ----------
if [ "${MOSAIC_LIVE_PROVIDER:-}" != "1" ]; then
echo "$LIVE_SKIP"
elif docker info >/dev/null 2>&1; then
# session fork (M11): teach in base, fork into child, child recalls;
# ancestor file count must be unchanged by the fork
@@ -512,9 +535,11 @@ grep -q "## Skillset" "$ONB/user/USER.md" && grep -q "## Pets" "$ONB/user/USER.m
&& check "canon sections present" 0 || check "canon sections present" 1
# Live recall: real pi, real phrasing - assert containment, not exactness.
# Needs Docker like the live task cases above. Without it the pair prints
# a skip and counts as neither a pass nor a failure.
if docker info >/dev/null 2>&1; then
# Needs Docker and MOSAIC_LIVE_PROVIDER=1 like the live fork cases above.
# Without either the pair prints a skip and counts as neither a pass nor a failure.
if [ "${MOSAIC_LIVE_PROVIDER:-}" != "1" ]; then
echo "$LIVE_SKIP"
elif docker info >/dev/null 2>&1; then
printf '{"taskVersion":1,"id":"t-user","prompt":"What is the user name? Reply with only the name.","session":"onb-check","timeoutSeconds":180}' > "$SANDBOX/recall-task.json"
RC=0
scripts/run-task.sh run "$SANDBOX/recall-task.json" >/dev/null 2>&1 || RC=$?
+99
View File
@@ -0,0 +1,99 @@
# Gate lanes
This tooling is for Sage's granted test gates on dragon-lin. It does not grant
itself a slot. A grant reference is an audit field, not an authentication token.
Until Sage adopts these lanes, the existing shared-slot procedure still applies.
Ask Sage with the lane, candidate SHA and exact command. Get a separate grant
for image preparation, suites, and the whole-slot paired proof. New state is
`/mnt/storage/scratch/mosaic-gate-lanes`. Old slot files are untouched.
Invoke the entry from the builder or canonical repository. The --checkout target
must be a separate clean scratch clone containing identical tooling source:
```bash
scripts/testing/gate-lane.sh run --lane a --candidate FULL_SHA \
--checkout /mnt/storage/scratch/ROW-RUN/wt \
--out /mnt/storage/scratch/ROW-RUN/out --grant SAGE_MESSAGE_ID \
--image-receipt /mnt/storage/scratch/PREP/image-receipt.json
```
Each run creates a private root like `/mnt/storage/scratch/ga-ABCDEF` (30 bytes).
An explicit `--temp-root` must already be private, outside `/tmp`, and no longer
than 30 UTF-8 bytes after resolving symlinks. The sandbox's own `/tmp` and HOME
are unchanged. Host HOME stays real. Steps get `PI_AUTH_FILE` naming a private `{}`
fixture in the temp root, and the Docker guard refuses a Compose run whose
`PI_AUTH_FILE` isn't a regular file inside the temp root (row 244). A listener covering loopback port 9, an
unsafe privileged-port threshold or unreadable host inventory refuses startup.
Lane a leases ports 61000..61127, lane b 61128..61255. Refuse any band overlapping
the host's current ephemeral range or a live listener. Port-0 fixtures with a
held server keep using port 0. Never replace the literal dead Board :9 URLs.
Two Node test files run per lane, including nested shell-suite Node runners.
```bash
scripts/testing/gate-lane.sh status
scripts/testing/gate-lane.sh recover --run-id RUN_NONCE --grant SAGE_MESSAGE_ID
```
Status reports each lane, the image lock, unfinished receipts and unmanaged
test/landing/build processes as LEGACY BUSY. It prints no command lines or
environment values. A dead owner with an unfinished receipt is quarantined even
when flock is free. Recovery takes both lanes and the exclusive image lock.
Unknown process-group, scope or Docker ownership refuses recovery. Do not remove
lock files, old receipts, or another run's resources to clear a refusal.
An image receipt records exact COPY input hashes/modes, image ID, release tag,
base digest and platform. Both concurrent candidates must match those files and
that same image ID. Rebuilding identical files is not equivalent. Warm gates
refuse implicit Docker builds/pulls and recheck the ID at each step and the end.
External same-uid commands can ignore these locks. Sage must exclude other
builders; this is test coordination, not a security boundary against that uid.
Only under an explicit image-build grant:
```bash
scripts/testing/gate-lane.sh prepare-image --lane a --candidate FULL_SHA \
--checkout /mnt/storage/scratch/ROW-PREP/wt \
--out /mnt/storage/scratch/ROW-PREP/out --grant SAGE_MESSAGE_ID
```
Preparation holds both lanes and the exclusive image lock. Its build log,
receipt, count and wall time are separate from warm gate timings. The current
Containerfile uses a mutable FROM tag and apt indexes. A missing base digest
refuses provenance publication. No credential file is copied or printed.
Run records are immutable JSON files published by link after fsync. Every step
has a log, UTC/monotonic times, exit code, pass/fail/skip counts and log hash.
The first failed step stops the gate. Cleanup touches only exact project labels,
recorded process identities and fixture paths. A process leak fails the gate
even when cleanup succeeds. Failed logs and temp roots remain for diagnosis.
A cleanup failure leaves the lane quarantined. GATE_PASSED requires all steps
and cleanup. Required prerequisite skips fail the run; optional skips remain
visible and require acceptance review.
Home metadata is evidence only. Assert only on entries attributable to this
run's nonce or fixture paths. Other sessions normally change ~/.claude, ~/.pi
and ~/.cache, so there is no global unchanged-home assertion.
The row 197 acceptance packet is `agents/herb/work/queue-197/`: two candidate
solo baselines followed by three overlapping pairs, lane assignment swapped in
pair two. Keep later paired results after adoption. Six green gates establish
the initial acceptance bar, not a reliable estimate of rare flakes.
The preflight unit fixture uses the canonical checkout's existing node_modules
at `/mnt/storage/src/mosaic-stack/node_modules` through the required plain
symlink. It does not install dependencies. Unit run roots use the `gt-` prefix;
real lane roots use `ga-` or `gb-`. Resource helper tests inside a lane lease only
that lane's band through MOSAIC_GATE_STATE, including their child test files.
The exhaustion probe temporarily narrows its process-local band to a port it
already leased, so it never reserves the whole shared band.
`closed` call-site options are accepted for compatibility; every dynamic lease
reserves an unbound endpoint. Listener fixtures decide when to bind and close.
Fixture child caches put TMPDIR under `tmp/` next to `jiti/` and `cache/`.
Outside a lane, the resource helper file uses lane a with the shared default
state `/mnt/storage/scratch/mosaic-gate-lanes`. It leaves stable lock files in
place. A single-file run therefore respects a live lane-a gate's closed leases.
Its unrelated-session probe bounds both polling children at five seconds.
+230
View File
@@ -0,0 +1,230 @@
import { createHash } from 'node:crypto';
import { realpathSync, existsSync, statSync, readdirSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { shortRoot, checkHost, hostFacts, refuse, within, SCRATCH, identity } from './lane-state.mjs';
const BUILDER=resolve(dirname(fileURLToPath(import.meta.url)),'../..');
const git=(root,...args)=>execFileSync('git',['-C',root,...args],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
export function validateRun(o){
if(!['a','b'].includes(o.lane))refuse('invalid lane');
if(typeof o.grant!=='string'||!o.grant.trim())refuse('missing grant reference');
for(const key of ['checkout','out','stateRoot'])if(typeof o[key]!=='string'||!o[key])refuse(`missing ${key}`);
const tempRoot=shortRoot(o.tempRoot);
const checkout=realpathSync(o.checkout);
if(checkout===BUILDER||checkout==='/mnt/storage/src/mosaic-stack'||!within(SCRATCH,checkout))refuse('canonical or builder checkout');
if(!/^[a-f0-9]{40}$/.test(o.candidate)||git(checkout,'rev-parse','HEAD')!==o.candidate)refuse('wrong candidate SHA');
const changed=git(checkout,'status','--porcelain','--untracked-files=normal').split('\n').filter(x=>x&&x!=='?? node_modules');
if(changed.length)refuse('dirty checkout');
if(!existsSync(join(checkout,'node_modules'))||!statSync(join(checkout,'node_modules')).isDirectory())refuse('missing node_modules');
const out=resolve(o.out);if(existsSync(out))refuse('output already exists');
if(!within(dirname(checkout),out)||!within(SCRATCH,realpathSync(dirname(out))))refuse('output escapes run scratch');
const stateRoot=resolve(o.stateRoot);if(!within(SCRATCH,stateRoot))refuse('state outside scratch');
checkHost(o.lane,o.hostFacts??hostFacts());
return {...o,tempRoot,checkout,out,stateRoot};
}
export function validateTooling(checkout){
for(const name of readdirSync(join(BUILDER,'scripts/testing')).filter(n=>/\.(mjs|sh)$/.test(n))){
const candidate=join(checkout,'scripts/testing',name),launcher=join(BUILDER,'scripts/testing',name);
if(!existsSync(candidate)||!readFileSync(candidate).equals(readFileSync(launcher)))refuse('candidate tooling differs from launcher');
}
}
export function discoverSteps(checkout,concurrency=2){
if(concurrency!==2)refuse('test concurrency must be 2');
const steps=[];
for(const p of readdirSync(join(checkout,'packages')).sort())if(p!=='queue'&&existsSync(join(checkout,'packages',p,'tests'))&&statSync(join(checkout,'packages',p,'tests')).isDirectory())steps.push({name:`pkg-${p}`,cwd:join(checkout,'packages',p),argv:['node','--test','--test-concurrency=2',...readdirSync(join(checkout,'packages',p,'tests')).filter(n=>n.endsWith('.test.mjs')).sort().map(n=>`tests/${n}`)]});
for(const f of readdirSync(join(checkout,'scripts')).sort())if(/^test-.+\.sh$/.test(f)&&statSync(join(checkout,'scripts',f)).isFile())steps.push({name:`script-${f.slice(5,-3)}`,cwd:checkout,argv:['bash',`scripts/${f}`]});
return steps;
}
export async function executeSteps(steps,{out,env,record,onChild=()=>{},signal}){
const {openSync,closeSync}=await import('node:fs');const {spawn}=await import('node:child_process');
for(const step of steps){
if(signal?.aborted)return {rc:130};
const begin=performance.now(),startMs=Date.now();const fd=openSync(join(out,`${step.name}.log`),'wx',0o600);
// Keep the supervisor private while matching the legacy test environment.
const child=spawn('/bin/bash',['-c','umask 022; exec "$@"','gate-step',...step.argv],{cwd:step.cwd,env,detached:true,stdio:['ignore',fd,fd]});closeSync(fd);
onChild(child);record({kind:'child',group:child.pid,owner:identity(child.pid)});
let escalation;
const abort=()=>{try{process.kill(-child.pid,'SIGTERM');}catch{};escalation=setTimeout(()=>{try{process.kill(-child.pid,'SIGKILL');}catch{}},3000);escalation.unref();};
signal?.addEventListener('abort',abort,{once:true});
const rc=await new Promise(resolve=>{child.once('error',()=>resolve(127));child.once('close',(code,sig)=>resolve(signal?.aborted?130:(code??(sig?128:1))));});
clearTimeout(escalation);signal?.removeEventListener('abort',abort);onChild(null);
const bytes=readFileSync(join(out,`${step.name}.log`)),text=bytes.toString('utf8');
const counts={};for(const name of ['pass','fail','skipped']){const m=text.match(new RegExp('^(?:ℹ |# )'+name+' (\\d+)','m'));if(m)counts[name]=Number(m[1]);}
if(counts.pass===undefined){const summaries=[...text.matchAll(/(\d+) passed, (\d+) failed/g)];const last=summaries.at(-1);if(last){counts.pass=Number(last[1]);counts.fail=Number(last[2]);}}
const requiredSkips=text.split('\n').filter(line=>/^skip\b/i.test(line)&&/(docker|daemon|sandbox|systemd|credential)/i.test(line));
record({kind:'step',name:step.name,startMs,endMs:Date.now(),wallMs:performance.now()-begin,rc,counts,requiredSkips,logSha256:createHash('sha256').update(bytes).digest('hex')});
if(rc!==0||requiredSkips.length)return {rc:rc||2};
}
return {rc:0};
}
export async function executePinnedSteps(steps,options,{receipt,checkout,inspect,afterStep=()=>{}}){
for(const step of steps){
verifyImage(receipt,checkout,inspect);
const result=await executeSteps([step],options);afterStep(step);
if(result.rc){verifyImage(receipt,checkout,inspect);return {...result,imageBuilds:0};}
}
verifyImage(receipt,checkout,inspect);return {rc:0,imageBuilds:0};
}
import { mkdirSync, lstatSync, fstatSync, mkdtempSync, writeFileSync, readFileSync, openSync, readlinkSync, rmdirSync, chmodSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
import { status, appendRecord, runRecords, processInventory, sameProcess, nonce } from './lane-state.mjs';
import { verifyImage, imageFingerprint, inspectImage, prepareImage } from './lane-image.mjs';
import { cleanupProject } from './lane-docker.mjs';
import { homeMetadata, attributeEntries, socketInventory } from './lane-evidence.mjs';
function options(argv){
const result={verb:'run',stateRoot:'/mnt/storage/scratch/mosaic-gate-lanes',lane:'a',exclusive:false};
const names={'--state-root':'stateRoot','--lane':'lane','--candidate':'candidate','--checkout':'checkout','--out':'out','--temp-root':'tempRoot','--grant':'grant','--image-receipt':'imageReceipt','--run-id':'runId'};
const args=[...argv];if(args[0]&&!args[0].startsWith('--'))result.verb=args.shift();
if(!['run','status','recover','prepare-image','lock-dir'].includes(result.verb))refuse('unknown verb');
while(args.length){const key=args.shift();if(key==='--exclusive'){result.exclusive=true;continue;}if(!names[key]||!args.length)refuse('invalid or missing option');result[names[key]]=args.shift();}
return result;
}
function stateDirectory(path,create=false){
const root=resolve(path);if(!within(SCRATCH,root)||root===SCRATCH)refuse('state must be private scratch');
if(create){const parent=realpathSync(dirname(root));if(!within(SCRATCH,parent))refuse('state parent escapes scratch');mkdirSync(root,{recursive:true,mode:0o700});}
if(existsSync(root)){const st=lstatSync(root);if(st.isSymbolicLink()||!st.isDirectory()||st.uid!==process.getuid()||(st.mode&0o077))refuse('state is not a private owned directory');
for(const n of ['a.lock','b.lock','image.lock'])if(existsSync(join(root,n))&&!lstatSync(join(root,n)).isFile())refuse('lock is not an ordinary file');
}
return root;
}
export function verifyLocks(o){
for(const [fd,name] of o.exclusive?[[3,'a.lock'],[4,'b.lock'],[5,'image.lock']]:[[3,`${o.lane}.lock`],[5,'image.lock']]){
let opened,path;try{opened=fstatSync(fd);path=lstatSync(join(o.stateRoot,name));}catch{refuse('lock descriptors missing');}
if(!opened.isFile()||!path.isFile()||opened.ino!==path.ino||opened.dev!==path.dev||path.uid!==process.getuid())refuse('lock file replaced');
// A separate open must fail while our shared/exclusive lock is held.
const r=spawnSync('flock',['-n','-x',join(o.stateRoot,name),'true']);if(r.status!==1)refuse('lock descriptor is not locked');
}
}
export function activeConflicts(o,observations={}){
const current=status(o.stateRoot,{...observations,ignoreLocks:o.exclusive?['a','b','image']:[o.lane,'image']});
if(current.legacy.length)refuse('LEGACY BUSY: unmanaged gate, test or Docker process');
for(const lane of o.exclusive?['a','b']:[o.lane])if(current[lane].state!=='CLEAR')refuse(`${lane} ${current[lane].state}`);
for(const r of runRecords(o.stateRoot)){const start=r.records.find(e=>e.kind==='start');if(!r.records.some(e=>['terminal','recovered'].includes(e.kind)&&e.clean)&&start?.checkout===o.checkout)refuse('checkout already owned');}
}
export function cleanEnv(o,run,docker){
const kept=['PATH','HOME','USER','LOGNAME','SHELL','LANG','LC_ALL','LC_CTYPE','TZ','TERM','XDG_RUNTIME_DIR','DBUS_SESSION_BUS_ADDRESS','DOCKER_HOST','CHROMIUM'];
const env=Object.fromEntries(kept.filter(k=>typeof process.env[k]==='string').map(k=>[k,process.env[k]]));
Object.assign(env,{TMPDIR:o.tempRoot,TMP:o.tempRoot,TEMP:o.tempRoot,MOSAIC_GATE_LANE:o.lane,MOSAIC_GATE_STATE:o.stateRoot,MOSAIC_TEST_CONCURRENCY:'2',COMPOSE_PROJECT_NAME:run.project,JITI_FS_CACHE:join(o.tempRoot,'jiti'),XDG_CACHE_HOME:join(o.tempRoot,'cache'),MOSAIC_CONFIG:join(o.tempRoot,'missing-config.json'),MOSAIC_GATE_DOCKER_RECORD:join(o.out,'docker.json'),
// A step reaching Compose mounts this fixture, never the host's pi auth file (row 244).
PI_AUTH_FILE:join(o.tempRoot,'pi-auth.json')});
env.PATH=`${join(o.out,'bin')}:${env.PATH??'/usr/bin:/bin'}`;
return env;
}
function fixtureProcesses(run){
const all=processInventory(),ancestors=new Set([process.pid]);let parent=process.pid;
while(parent){const p=all.find(p=>p.pid===parent);if(!p||ancestors.has(p.ppid))break;ancestors.add(p.ppid);parent=p.ppid;}
return all.filter(p=>{
if(ancestors.has(p.pid)||p.state==='Z')return false;
let cwd;try{cwd=readlinkSync(`/proc/${p.pid}/cwd`);}catch{}
return p.argv.some(a=>typeof a==='string'&&(a===run.tempRoot||a.startsWith(run.tempRoot+'/')))||cwd&&(within(run.tempRoot,cwd)||run.checkout&&within(run.checkout,cwd));
});
}
async function cleanProcesses(run,record){
const before=fixtureProcesses(run);
if(run.stateRoot&&run.id){
const procs=processInventory(),known=new Set(before.map(p=>p.pid));
const {records}=await import('./lane-state.mjs');
for(const child of records(join(run.stateRoot,'runs',run.id)).filter(e=>e.kind==='child')){
const members=procs.filter(p=>p.group===child.group&&p.state!=='Z');
if(members.length&&sameProcess(child.owner,procs.find(p=>p.pid===child.owner?.pid))){
try{process.kill(-child.group,'SIGKILL');}catch(e){if(e.code!=='ESRCH')throw e;}
}else if(members.some(p=>!known.has(p.pid)))refuse('unproven orphan process-group ownership');
}
}
record({kind:'leftover-processes',objects:before.map(({pid,start,boot})=>({pid,start,boot}))});
for(const p of before){if(!sameProcess(p))continue;let raw;try{raw=readFileSync(`/proc/${p.pid}/cgroup`,'utf8');}catch(e){if(e.code==='ENOENT')continue;throw e;}const scope=raw.trim().split('/').find(x=>/^mosaic-chat-[a-z0-9]+\.scope$/.test(x));
if(scope){const shown=execFileSync('systemctl',['--user','show',scope,'-p','InvocationID','--value'],{encoding:'utf8'}).trim();if(!/^[a-f0-9]{32}$/.test(shown))refuse('scope identity unknown');record({kind:'scope-cleanup',unit:scope,invocation:shown});execFileSync('systemctl',['--user','kill','--signal=SIGKILL',scope],{stdio:'pipe'});}
if(sameProcess(p))try{process.kill(p.pid,'SIGKILL');}catch(e){if(e.code!=='ESRCH')throw e;}
}
for(let n=0;n<30;n++){if(!fixtureProcesses(run).some(p=>p.state!=='Z'))return before.length===0;await new Promise(r=>setTimeout(r,100));}
refuse('owned fixture process remains');
}
async function recoverRun(o){
if(!o.grant)refuse('missing recovery grant');
if(!/^[a-f0-9]{12}$/.test(o.runId??''))refuse('invalid run ID');
const r=runRecords(o.stateRoot).find(r=>r.id===o.runId);if(!r)refuse('run not found');
const start=r.records.find(x=>x.kind==='start');if(start?.mode==='prepare-image'&&status(o.stateRoot).legacy.length)refuse('image builder still active or unknown');if(!start||sameProcess(start.owner))refuse('run owner live or unknown');
if(r.records.some(x=>['terminal','recovered'].includes(x.kind)&&x.clean))refuse('run already clean');
shortRoot(start.tempRoot);const record=e=>appendRecord(r.dir,e);
const procs=processInventory(),known=new Set(fixtureProcesses(start).map(p=>p.pid));
for(const event of r.records.filter(e=>e.kind==='child')){
const members=procs.filter(p=>p.group===event.group&&p.state!=='Z');
if(members.length&&!sameProcess(event.owner,procs.find(p=>p.pid===event.owner?.pid))&&members.some(p=>!known.has(p.pid)))refuse('orphan process-group ownership unknown');
if(members.length&&sameProcess(event.owner)){process.kill(-event.group,'SIGKILL');}
}
await cleanProcesses(start,record);cleanupProject(start,start.docker);
appendRecord(r.dir,{kind:'recovered',clean:true,grant:o.grant,owner:identity()});return {recovered:r.id};
}
async function runGate(input){
for(const key of ['checkout','out','candidate','grant','imageReceipt'])if(!input[key])refuse(`missing ${key}`);
const automatic=!input.tempRoot,tempRoot=input.tempRoot??mkdtempSync(`/mnt/storage/scratch/g${input.lane}-`);
let o,receipt,docker;
try{
o=validateRun({...input,tempRoot});
validateTooling(o.checkout);
activeConflicts(o);
receipt=JSON.parse(readFileSync(o.imageReceipt,'utf8'));verifyImage(receipt,o.checkout);
docker=execFileSync('which',['docker'],{encoding:'utf8'}).trim();
}catch(e){if(automatic)rmdirSync(tempRoot);throw e;}
const id=nonce(),run={kind:'start',id,stateRoot:o.stateRoot,lane:o.lane,exclusive:o.exclusive,owner:identity(),grant:o.grant,candidate:o.candidate,checkout:o.checkout,tempRoot:o.tempRoot,out:o.out,project:`mg197-${o.lane}-${id}`,tag:receipt.tag,imageId:receipt.imageId,docker};
const dir=join(o.stateRoot,'runs',id);appendRecord(dir,run);const record=e=>{appendRecord(dir,e);if(e.kind==='step')process.stdout.write(`${e.name} rc=${e.rc} wall_ms=${Math.round(e.wallMs)} pass=${e.counts.pass??'-'} fail=${e.counts.fail??'-'} skipped=${e.counts.skipped??'-'}\n`);};
const begin=performance.now(),startMs=Date.now();let rc=1,clean=false,current=null;
const aborter=new AbortController();const stop=()=>aborter.abort();for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,stop);
try{
mkdirSync(o.out,{mode:0o700});mkdirSync(join(o.out,'bin'),{mode:0o700});
writeFileSync(join(o.out,'docker.json'),JSON.stringify({...run,docker}),{mode:0o600,flag:'wx'});
const guard=join(BUILDER,'scripts/testing/lane-docker.mjs');
// Launch the guard directly so it validates every real Docker invocation.
writeFileSync(join(o.out,'bin','docker'),`#!/bin/sh\nexec '${process.execPath.replaceAll("'","'\\''")}' '${guard.replaceAll("'","'\\''")}' "$@"\n`,{mode:0o700,flag:'wx'});
const auth=join(o.tempRoot,'pi-auth.json');writeFileSync(auth,'{}',{mode:0o600,flag:'wx'});chmodSync(auth,0o600);
const steps=discoverSteps(o.checkout);record({kind:'plan',steps});
const before=homeMetadata(process.env.HOME);record({kind:'home-before',entries:before});
const env=cleanEnv(o,run,docker);
const result=await executePinnedSteps(steps,{out:o.out,env,record,onChild:c=>current=c,signal:aborter.signal},{receipt,checkout:o.checkout,afterStep:()=>record({kind:'sockets',entries:socketInventory(o.tempRoot)})});
rc=result.rc;
const after=homeMetadata(process.env.HOME);record({kind:'home-after',entries:after});
const prior=new Map(before.map(e=>[e.path,JSON.stringify(e)]));const changed=after.filter(e=>prior.get(e.path)!==JSON.stringify(e));
if(attributeEntries(changed,{...run,nonce:id,fixturePaths:[]}).owned.length)refuse('lane-attributed real HOME write');
if(git(o.checkout,'diff','--name-only'))refuse('tracked checkout changed during gate');
const noLeaks=await cleanProcesses(run,record);cleanupProject(run,docker);clean=true;if(!noLeaks&&rc===0)rc=1;
}catch(e){record({kind:'failure',message:e.message});process.stderr.write(`${e.message}\n`);rc=rc||1;
try{await cleanProcesses(run,record);cleanupProject(run,docker);clean=true;}catch(error){record({kind:'cleanup-failed',message:error.message});}
}finally{
if(current){try{process.kill(-current.pid,'SIGKILL');}catch{}}
for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.off(signal,stop);
record({kind:'terminal',clean,rc,startMs,endMs:Date.now(),wallMs:performance.now()-begin,imageBuilds:0});
}
if(rc===0&&clean)process.stdout.write('GATE_PASSED\n');return rc;
}
async function main(argv){
const locked=argv[0]==='--locked';if(locked)argv=argv.slice(1);
if(argv[0]==='lock-dir'){
const o=options(argv.slice(1));if(!o.grant)refuse('missing grant reference');if(!['a','b'].includes(o.lane))refuse('invalid lane');process.stdout.write(stateDirectory(o.stateRoot,true)+'\n');return 0;
}
const o=options(argv);o.stateRoot=stateDirectory(o.stateRoot);
if(o.verb==='status'){process.stdout.write(JSON.stringify(status(o.stateRoot))+'\n');return 0;}
if(!locked)refuse('use gate-lane.sh for locked operations');
if(['recover','prepare-image'].includes(o.verb))o.exclusive=true;
verifyLocks(o);
if(o.verb==='recover'){process.stdout.write(JSON.stringify(await recoverRun(o))+'\n');return 0;}
if(o.verb==='prepare-image'){
for(const key of ['checkout','out','candidate','grant'])if(!o[key])refuse(`missing ${key}`);
const checked=validateRun({...o,tempRoot:o.tempRoot??mkdtempSync('/mnt/storage/scratch/ga-')});activeConflicts(checked);
const id=nonce(),dir=join(checked.stateRoot,'runs',id),docker=execFileSync('which',['docker'],{encoding:'utf8'}).trim();
appendRecord(dir,{kind:'start',mode:'prepare-image',lane:checked.lane,exclusive:true,owner:identity(),grant:checked.grant,candidate:checked.candidate,checkout:checked.checkout,tempRoot:checked.tempRoot,out:checked.out,project:`mg197-${checked.lane}-${id}`,docker});
mkdirSync(checked.out,{mode:0o700});
try{
const receipt=prepareImage(checked.checkout,checked);
writeFileSync(join(checked.out,'image-receipt.json'),JSON.stringify(receipt,null,2)+'\n',{mode:0o600,flag:'wx'});
appendRecord(dir,{kind:'terminal',clean:true,rc:0,imageId:receipt.imageId,builds:1,wallMs:receipt.buildWallMs});
process.stdout.write(JSON.stringify({imageReceipt:join(checked.out,'image-receipt.json'),imageId:receipt.imageId,buildWallMs:receipt.buildWallMs})+'\n');return 0;
}catch(e){appendRecord(dir,{kind:'terminal',clean:false,rc:1,message:e.message});throw e;}
}
return runGate(o);
}
if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href)main(process.argv.slice(2)).then(rc=>process.exitCode=rc).catch(e=>{process.stderr.write(`${e.message}\n`);process.exitCode=2;});
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
entry="$(cd "$(dirname "$0")" && pwd -P)/gate-lane.mjs"
verb="${1:-run}"
if [ "$verb" = status ]; then exec node "$entry" "$@"; fi
state="$(node "$entry" lock-dir "$@")"
lane=a
exclusive=0
args=("$@")
for ((i=0;i<${#args[@]};i++)); do
case "${args[i]}" in
--lane) lane="${args[i+1]:-}" ;;
--exclusive) exclusive=1 ;;
esac
done
case "$verb" in recover|prepare-image) exclusive=1;; esac
case "$lane" in a|b) ;; *) echo 'gate-lane: refused: invalid lane' >&2; exit 2;; esac
if [ "$exclusive" = 1 ]; then
exec 3>>"$state/a.lock" 4>>"$state/b.lock" 5>>"$state/image.lock"
flock -n 3 && flock -n 4 && flock -n -x 5 || { echo 'gate-lane: refused: lane or image lock busy' >&2; exit 2; }
else
exec 3>>"$state/$lane.lock" 5>>"$state/image.lock"
flock -n 3 && flock -n -s 5 || { echo 'gate-lane: refused: lane or image lock busy' >&2; exit 2; }
fi
exec node "$entry" --locked "$@"
+68
View File
@@ -0,0 +1,68 @@
import { execFileSync, spawnSync } from 'node:child_process';
import { readFileSync, writeFileSync, linkSync, unlinkSync, openSync, closeSync, fstatSync, realpathSync, statSync, constants } from 'node:fs';
import { join, isAbsolute } from 'node:path';
import { pathToFileURL } from 'node:url';
import { refuse, shortRoot, nonce, within } from './lane-state.mjs';
const HOST_PI_AUTH='/home/jwoltje/.pi/agent/auth.json';
function composeFiles(run){
if(!/^sha256:[a-f0-9]{64}$/.test(run.imageId??''))refuse('invalid pinned image ID');
if(typeof run.checkout!=='string'||!isAbsolute(run.checkout))refuse('invalid candidate checkout');
return [join(run.checkout,'compose.yaml'),join(shortRoot(run.tempRoot),'compose-no-build.yaml')];
}
export function writeComposeOverride(run){
const [,file]=composeFiles(run);
const text=`services:\n mosaic-agent:\n build: !reset null\n image: ${JSON.stringify(run.imageId)}\n`;
// Publish a complete file without replacing another guard's override.
const pending=file+'.'+nonce();
try{
writeFileSync(pending,text,{mode:0o600,flag:'wx'});
try{linkSync(pending,file);}catch(e){if(e.code!=='EEXIST')throw e;}
}finally{try{unlinkSync(pending);}catch(e){if(e.code!=='ENOENT')throw e;}}
let fd;
try{
fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
const st=fstatSync(fd);
if(!st.isFile()||st.uid!==process.getuid()||(st.mode&0o777)!==0o600||st.size!==Buffer.byteLength(text)||readFileSync(fd,'utf8')!==text)
refuse('unsafe or changed Compose override');
}catch{refuse('unsafe or changed Compose override');}
finally{if(fd!==undefined)closeSync(fd);}
return file;
}
// compose.yaml mounts PI_AUTH_FILE, defaulting to the host's real pi auth file (row 244).
function laneAuthFile(env,run){
const file=env.PI_AUTH_FILE;let real,root;
try{real=realpathSync(file);root=realpathSync(run.tempRoot);}catch{refuse('Compose auth mount not a lane fixture');}
if(!isAbsolute(file)||file===HOST_PI_AUTH||real===HOST_PI_AUTH||!within(root,real)||!statSync(real).isFile())refuse('Compose auth mount not a lane fixture');
}
export function guardDocker(argv,run,env=process.env){
if(argv[0]==='info'&&argv.length===1)return argv;
if(argv[0]==='image'&&argv[1]==='inspect'&&argv[2]===run.tag&&argv.length===3)return argv;
if(argv[0]==='compose'&&argv[1]==='run'){
const service=argv.indexOf('mosaic-agent',2);
if(service<0||argv.slice(2,service).some(a=>!['--rm','-T','--no-deps','--interactive','-i'].includes(a)))refuse('Docker Compose run options not allowed');
laneAuthFile(env,run);
const [base,override]=composeFiles(run);
return ['compose','-f',base,'-f',override,'run','--pull','never','--no-deps',...argv.slice(2)];
}
refuse('Docker mutation or unowned command');
}
export function cleanupProject(run,docker='docker'){
if(!/^mg197-[ab]-[a-f0-9]{12}$/.test(run.project))refuse('invalid Docker ownership project');
const invoke=typeof docker==='function'?docker:(args)=>execFileSync(docker,args,{encoding:'utf8',stdio:['ignore','pipe','pipe']});
const removed=[];
for(const [kind,list,filter] of [['container',['ps','-aq'],'com.docker.compose.project'],['network',['network','ls','-q'],'com.docker.compose.project']]){
const ids=invoke([...list,'--filter',`label=${filter}=${run.project}`]).trim().split(/\s+/).filter(Boolean);
for(const id of ids){if(!/^[a-f0-9]{12,64}$/.test(id))refuse('invalid Docker resource ID');
const label=invoke([kind,'inspect',id,'--format',`{{index .${kind==='container'?'Config.':''}Labels "${filter}"}}`]).trim();
if(label!==run.project)refuse('foreign Docker resource');
invoke(kind==='container'?['container','rm','-f',id]:['network','rm',id]);removed.push({kind,id});
}
const left=invoke([...list,'--filter',`label=${filter}=${run.project}`]).trim();if(left)refuse('Docker cleanup incomplete');
}
return {clean:true,removed};
}
if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href){
try{const run=JSON.parse(readFileSync(process.env.MOSAIC_GATE_DOCKER_RECORD,'utf8'));if(process.env.COMPOSE_PROJECT_NAME!==run.project)refuse('Compose project mismatch');
const argv=guardDocker(process.argv.slice(2),run,process.env);if(argv[0]==='compose')writeComposeOverride(run);const result=spawnSync(run.docker,argv,{stdio:'inherit'});process.exit(result.status??1);
}catch(e){process.stderr.write(`${e.message}\n`);process.exit(2);}
}
+37
View File
@@ -0,0 +1,37 @@
import { readdirSync, lstatSync, readlinkSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { within, refuse } from './lane-state.mjs';
export function attributeEntries(metadata,run){
const result={owned:[],unrelated:[],unknown:[]};
for(const e of metadata){const owns=e.path.includes(run.nonce)||e.link&&within(run.tempRoot,e.link)||(run.fixturePaths??[]).some(path=>within(path,e.path)||e.link&&within(path,e.link));result[owns?'owned':'unrelated'].push(e);}
return result;
}
export function homeMetadata(home){
const out=[];
for(const name of ['.claude','.pi','.cache']){
const root=join(home,name);if(!existsSync(root))continue;
// Two levels are evidence only. Never open or hash file contents.
const walk=(dir,depth)=>{for(const d of readdirSync(dir,{withFileTypes:true})){const path=join(dir,d.name);try{const st=lstatSync(path);out.push({path,size:st.size,mtime:st.mtimeMs,mode:st.mode,link:st.isSymbolicLink()?readlinkSync(path):null});if(d.isDirectory()&&depth)walk(path,depth-1);}catch{}}};
walk(root,1);
}
return out;
}
export function socketInventory(root){const out=[];const walk=dir=>{for(const d of readdirSync(dir,{withFileTypes:true})){const p=join(dir,d.name);if(d.isDirectory())walk(p);else if(d.isSocket()){const bytes=Buffer.byteLength(p);out.push({path:p,bytes});if(bytes>107)refuse('socket path exceeds 107 bytes');}}};walk(root);return out;}
export function summarizePairs(solos,pairs){
const byCandidate=new Map(solos.map(r=>[r.candidate,r]));
if(solos.length!==2||byCandidate.size!==2||solos.some(r=>!Number.isFinite(r.wallMs)||r.wallMs<=0))refuse('solo timing invalid');
if(pairs.length!==3)refuse('proof needs three pairs');
let first;
return pairs.map((pair,index)=>{
if(pair.length!==2||pair[0].candidate===pair[1].candidate||new Set(pair.map(r=>r.lane)).size!==2||pair.some(r=>!['a','b'].includes(r.lane)))refuse('pair needs different candidates and lanes');
for(const run of pair){
if(!run.clean||run.rc!==0||!byCandidate.has(run.candidate))refuse('proof gate failed');
if(!Number.isFinite(run.startMs)||!Number.isFinite(run.endMs)||run.endMs<=run.startMs)refuse('paired timing invalid');
}
if(index===0)first=new Map(pair.map(r=>[r.candidate,r.lane]));
if(index===1&&pair.some(r=>first.get(r.candidate)===r.lane))refuse('second pair must swap lanes');
if(Math.max(...pair.map(r=>r.startMs))>=Math.min(...pair.map(r=>r.endMs)))refuse('paired gates did not overlap');
const makespan=Math.max(...pair.map(r=>r.endMs))-Math.min(...pair.map(r=>r.startMs));
return {makespanMs:makespan,runs:pair.map(r=>({candidate:r.candidate,wallMs:r.endMs-r.startMs,slowdown:(r.endMs-r.startMs)/byCandidate.get(r.candidate).wallMs})),throughput:pair.reduce((n,r)=>n+byCandidate.get(r.candidate).wallMs,0)/makespan};
});
}
+33
View File
@@ -0,0 +1,33 @@
// Imported only by tests. The port holder lives until the fixture closes.
import { spawn } from 'node:child_process';
import { mkdirSync, realpathSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'node:net';
import { once } from 'node:events';
import { BANDS, within, SCRATCH, refuse } from './lane-state.mjs';
async function hold(file){
const child=spawn('flock',['-n','-E','73',file,'bash','-c','printf "held\\n"; IFS= read -r release'],{stdio:['pipe','pipe','pipe']});
let err='';child.stderr.on('data',c=>err+=c);const exited=once(child,'exit');
const ok=await new Promise((resolve,reject)=>{child.stdout.once('data',()=>resolve(true));child.once('exit',code=>code===73?resolve(false):reject(Error(`port lock holder exited ${code}: ${err}`)));child.once('error',reject);});
if(!ok)return null;
return async()=>{child.stdin.end('\n');await exited;};
}
async function probe(port){const s=createServer();try{await new Promise((resolve,reject)=>{s.once('error',reject);s.listen(port,'127.0.0.1',resolve);});const found=s.address().port;await new Promise(r=>s.close(r));return found;}catch(e){if(s.listening)await new Promise(r=>s.close(r));if(e.code==='EADDRINUSE')return null;throw e;}}
export async function leasePort(t,{lane=process.env.MOSAIC_GATE_LANE,stateRoot=process.env.MOSAIC_GATE_STATE}={}){
if(!lane){const port=await probe(0);return port;}
if(!BANDS[lane]||!stateRoot)refuse('invalid port lane state');
const state=realpathSync(stateRoot);if(!within(SCRATCH,state))refuse('port state outside scratch');
const dir=join(state,'ports',lane);mkdirSync(dir,{recursive:true,mode:0o700});
const [lo,hi]=BANDS[lane];
for(let port=lo;port<=hi;port++){
const release=await hold(join(dir,`${port}.lock`));if(!release)continue;
let actual;try{actual=await probe(port);}catch(e){await release();throw e;}
if(!actual){await release();continue;}
t.after(release);return port;
}
refuse('lane port band exhausted');
}
export function fixtureCacheEnv(root){
mkdirSync(root,{recursive:true,mode:0o700});const dir=realpathSync(root);for(const n of ['tmp','jiti','cache'])mkdirSync(join(dir,n),{recursive:true,mode:0o700});
return {TMPDIR:join(dir,'tmp'),JITI_FS_CACHE:join(dir,'jiti'),XDG_CACHE_HOME:join(dir,'cache')};
}
+44
View File
@@ -0,0 +1,44 @@
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, lstatSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { refuse } from './lane-state.mjs';
export function imageFingerprint(checkout){
const dockerfile=readFileSync(join(checkout,'Containerfile'),'utf8');
const copies=dockerfile.split('\n').filter(l=>/^\s*(COPY|ADD)\s/i.test(l)).map(l=>l.trim());
const expected=['COPY package.json package-lock.json ./','COPY contracts /opt/mosaic/contracts','COPY src /opt/mosaic/src','COPY adapters /opt/mosaic/adapters'];
if(JSON.stringify(copies)!==JSON.stringify(expected))refuse('image COPY inputs changed; manifest review required');
const hash=createHash('sha256');
const visit=rel=>{const path=join(checkout,rel),st=lstatSync(path);if(st.isSymbolicLink())refuse('image input is a symlink');hash.update(JSON.stringify([rel,st.mode&0o777,st.isDirectory()?'d':'f'])+'\n');if(st.isDirectory())for(const n of readdirSync(path).sort())visit(`${rel}/${n}`);else if(st.isFile())hash.update(readFileSync(path));else refuse('unsupported image input');};
for(const rel of ['Containerfile','package.json','package-lock.json','contracts','src','adapters'])visit(rel);
return hash.digest('hex');
}
export function inspectImage(tag,docker=process.env.MOSAIC_GATE_REAL_DOCKER??'docker'){
return JSON.parse(execFileSync(docker,['image','inspect',tag,'--format','{{json .}}'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}));
}
export function verifyImage(receipt,checkout,inspect=inspectImage){
if(!receipt||!/^sha256:[a-f0-9]{64}$/.test(receipt.imageId??'')||!/^.+@sha256:[a-f0-9]{64}$/.test(receipt.baseDigest??'')||!/^linux\/[a-z0-9_]+$/.test(receipt.platform??'')||!/^mosaic-poc-agent:[A-Za-z0-9_.-]+$/.test(receipt.tag??''))refuse('image provenance receipt invalid');
if(receipt.inputDigest!==imageFingerprint(checkout))refuse('image inputs differ');
const actual=inspect(receipt.tag);
if(actual.Id!==receipt.imageId)refuse('pinned image ID differs');
if(`${actual.Os}/${actual.Architecture}`!==receipt.platform)refuse('image platform differs');
const release=readFileSync(join(checkout,'RELEASE'),'utf8').trim();const pkg=JSON.parse(readFileSync(join(checkout,'package.json'),'utf8'));
if(pkg.dependencies?.['@earendil-works/pi-coding-agent']&&receipt.tag!==`mosaic-poc-agent:${pkg.dependencies['@earendil-works/pi-coding-agent']}-r${release}`)refuse('image tag differs from release identity');
return receipt;
}
// Called by the CLI only while both lanes and the image lock are exclusive.
export function prepareImage(checkout,{grant,tempRoot,out},invoke){
if(!grant)refuse('missing image preparation grant');
const inputDigest=imageFingerprint(checkout);
const pkg=JSON.parse(readFileSync(join(checkout,'package.json'),'utf8'));
const release=readFileSync(join(checkout,'RELEASE'),'utf8').trim();
if(!/^\d+\.\d+\.\d+$/.test(release))refuse('invalid image release');
const tag=`mosaic-poc-agent:${pkg.dependencies['@earendil-works/pi-coding-agent']}-r${release}`;
const run=invoke??((args)=>execFileSync('docker',args,{cwd:checkout,encoding:'utf8',stdio:['ignore','pipe','pipe'],env:{PATH:process.env.PATH,HOME:process.env.HOME,...(process.env.DOCKER_HOST?{DOCKER_HOST:process.env.DOCKER_HOST}:{}),COMPOSE_PROJECT_NAME:'mg197-image-prepare',MOSAIC_IMAGE_TAG:tag,MOSAIC_DATA_ROOT:join(tempRoot,'data'),MOSAIC_PROVIDER:'fixture',MOSAIC_MODEL:'fixture',PI_AUTH_FILE:'/dev/null'}}));
const start=performance.now();let buildOutput;try{buildOutput=run(['compose','build']);}catch(e){if(out)writeFileSync(join(out,'image-build.log'),String(e.stdout??'')+String(e.stderr??''),{mode:0o600,flag:'wx'});throw e;}if(out)writeFileSync(join(out,'image-build.log'),buildOutput,{mode:0o600,flag:'wx'});const buildWallMs=performance.now()-start;
if(inputDigest!==imageFingerprint(checkout))refuse('image inputs changed while building');
const actual=JSON.parse(run(['image','inspect',tag,'--format','{{json .}}']));
const base=JSON.parse(run(['image','inspect','node:24-bookworm-slim','--format','{{json .}}']));
const receipt={inputDigest,imageId:actual.Id,tag,baseDigest:base.RepoDigests?.find(x=>/@sha256:[a-f0-9]{64}$/.test(x)),platform:`${actual.Os}/${actual.Architecture}`,grant,builds:1,buildWallMs,at:new Date().toISOString()};
verifyImage(receipt,checkout,()=>actual);return receipt;
}
+77
View File
@@ -0,0 +1,77 @@
// Scratch-only lane ownership. No live Mosaic state is read or written.
import { realpathSync, statSync, lstatSync, existsSync, readdirSync, readFileSync, mkdirSync, openSync, writeSync, fsyncSync, closeSync, linkSync, unlinkSync, renameSync } from 'node:fs';
import { resolve, join, dirname, relative, isAbsolute } from 'node:path';
import { execFileSync, spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
export const SCRATCH='/mnt/storage/scratch';
export const BANDS={a:[61000,61127],b:[61128,61255]};
export function refuse(message){throw new Error(`gate-lane: refused: ${message}`);}
export function within(root,path){const r=relative(root,path);return r===''||(!r.startsWith('..')&&!isAbsolute(r));}
export function shortRoot(path){
let real;try{real=realpathSync(path);}catch{refuse('temporary root missing');}
if(real==='/tmp'||real.startsWith('/tmp/'))refuse('temporary root under /tmp');
if(Buffer.byteLength(real)>30)refuse('temporary root exceeds 30 bytes');
if(!within(SCRATCH,real)||!statSync(real).isDirectory()||statSync(real).uid!==process.getuid()||(statSync(real).mode&0o077))refuse('temporary root must be private owned scratch');
return real;
}
export function socketPath(path){if(Buffer.byteLength(path)>107)refuse('socket path exceeds 107 bytes');return path;}
export function hostFacts(){
const range=readFileSync('/proc/sys/net/ipv4/ip_local_port_range','utf8').trim().split(/\s+/).map(Number);
const unprivilegedPortStart=Number(readFileSync('/proc/sys/net/ipv4/ip_unprivileged_port_start','utf8').trim());
const text=execFileSync('ss',['-Hltn'],{encoding:'utf8'});
const listeners=text.trim().split('\n').filter(Boolean).map(line=>{const field=line.trim().split(/\s+/)[3];const m=field?.match(/^(.*):(\d+)$/);if(!m)refuse('unreadable listener inventory');return {address:m[1].replace(/^\[|\]$/g,''),port:Number(m[2])};});
return {ephemeral:range,unprivilegedPortStart,listeners};
}
export function checkHost(lane,facts){
if(!facts||!Array.isArray(facts.listeners)||facts.ephemeral?.length!==2||!facts.ephemeral.every(Number.isInteger))refuse('host inventory unavailable');
if(!Number.isInteger(facts.unprivilegedPortStart)||facts.unprivilegedPortStart<=9)refuse('unprivileged port range includes port 9');
if(facts.listeners.some(x=>x.port===9&&['127.0.0.1','0.0.0.0','*','::','::ffff:127.0.0.1'].includes(x.address.replace(/%[^%]+$/, ''))))refuse('live port 9 listener');
const [lo,hi]=BANDS[lane];const [first,last]=facts.ephemeral;
if(lo<=last&&hi>=first)refuse('lane port band overlaps ephemeral range');
if(facts.listeners.some(x=>x.port>=lo&&x.port<=hi))refuse('lane port band has a live listener');
}
export function identity(pid=process.pid){try{const s=readFileSync(`/proc/${pid}/stat`,'utf8');const f=s.slice(s.lastIndexOf(')')+2).split(' ');return {pid:Number(pid),start:f[19],boot:readFileSync('/proc/sys/kernel/random/boot_id','utf8').trim(),state:f[0]};}catch{return null;}}
export function sameProcess(a,b=identity(a?.pid)){return !!a&&!!b&&a.pid===b.pid&&a.start===b.start&&a.boot===b.boot&&b.state!=='Z';}
export function nonce(){return randomBytes(6).toString('hex');}
export function appendRecord(dir,record){
const publishedDir=dir;if(record.kind==='start'){if(existsSync(dir))refuse('run already exists');dir=dir+'.starting';}
mkdirSync(dir,{recursive:true,mode:0o700});
const file=join(dir,`${Date.now()}-${nonce()}-${record.kind}.json`);
const pending=file+'.pending',fd=openSync(pending,'wx',0o600);try{writeSync(fd,JSON.stringify({...record,at:new Date().toISOString()})+'\n');fsyncSync(fd);}finally{closeSync(fd);}
linkSync(pending,file);unlinkSync(pending);if(record.kind==='start')renameSync(dir,publishedDir);return join(publishedDir,file.split('/').at(-1));
}
export function records(dir){if(!existsSync(dir))return [];return readdirSync(dir).filter(n=>n.endsWith('.json')).sort().map(n=>{try{return JSON.parse(readFileSync(join(dir,n),'utf8'));}catch{refuse(`invalid run receipt ${n}`);}});}
export function runRecords(stateRoot){
const runs=join(stateRoot,'runs');if(!existsSync(runs))return [];
return readdirSync(runs).filter(id=>!id.endsWith('.starting')).map(id=>({id,dir:join(runs,id),records:records(join(runs,id))}));
}
export function processInventory(){
const out=[];
for(const pid of readdirSync('/proc').filter(n=>/^\d+$/.test(n))){try{const argv=readFileSync(`/proc/${pid}/cmdline`,'utf8').split('\0').filter(Boolean);const raw=readFileSync(`/proc/${pid}/stat`,'utf8');const f=raw.slice(raw.lastIndexOf(')')+2).split(' ');out.push({...identity(Number(pid)),ppid:Number(f[1]),group:Number(f[2]),argv});}catch{}}
return out;
}
export function status(stateRoot,observations={}){
const procs=observations.processes??processInventory();const runs=runRecords(stateRoot);
const answer={a:{state:'CLEAR'},b:{state:'CLEAR'},image:'free',legacy:[],at:new Date().toISOString()};
const owned=new Set();
for(const run of runs){const start=run.records.find(x=>x.kind==='start');if(!start)refuse('run receipt has no start');const clean=run.records.some(x=>['terminal','recovered'].includes(x.kind)&&x.clean===true);
if(clean)continue;
const live=procs.find(p=>sameProcess(start.owner,p));const state=live?'BUSY':'QUARANTINED';
for(const lane of start.exclusive?['a','b']:[start.lane]){if(!BANDS[lane])refuse('invalid lane receipt');answer[lane]={state,run:run.id};}
answer.image=start.exclusive?'exclusive':'shared';
if(live){owned.add(live.pid);let changed=true;while(changed){changed=false;for(const p of procs)if(owned.has(p.ppid)&&!owned.has(p.pid)){owned.add(p.pid);changed=true;}}}
}
for(const p of procs){if(p.state==='Z'||p.pid===process.pid||owned.has(p.pid))continue;
const args=p.argv??[];const executable=args[0]?.split('/').at(-1);
const test=(executable==='node'&&args.includes('--test'))||args.some(a=>/^(?:.*\/)?(?:gate\.sh|test-[a-z-]+\.sh)$/.test(a))||(executable==='docker'&&(args.includes('compose')||args.includes('build')))||args.some(a=>a.endsWith('/landing.mjs')&&args.includes('gate'))||args.some(a=>/(?:^|\/)[^/]*mutant[^/]*\.(?:mjs|py|sh)$/.test(a));
if(test)answer.legacy.push({pid:p.pid,start:p.start});
}
for(const lane of ['a','b','image']){
if(observations.ignoreLocks?.includes(lane)||!existsSync(join(stateRoot,`${lane}.lock`)))continue;
const lock=lstatSync(join(stateRoot,`${lane}.lock`));if(!lock.isFile())refuse('lock is not an ordinary file');
const probe=spawnSync('flock',['-n','-x',join(stateRoot,`${lane}.lock`),'true']);
if(probe.status!==0&&probe.status!==1)refuse('cannot inspect lane lock');
if(probe.status===1){if(lane==='image')answer.image=answer.image==='free'?'held':answer.image;else if(answer[lane].state==='CLEAR')answer[lane]={state:'BUSY',run:null};}
}
return answer;
}