Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56233337bc | ||
|
|
8039fc1a99 | ||
|
|
471717c595 |
No files matched your search
@@ -5554,3 +5554,5 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r
|
||||
2026-10-24T15:55:22Z | Sage | Row 225 (#1726, C2-10b, Fethry on Codex, Dewey) LANDED 9b6345466. Before: refactor at c63801959. After: Fethry's r2 candidate c1f9dfd3c (11 commits on 963303403) cherry-picked onto c63801959. 8 commits are equal by range-diff; the other 3 differ only in the append-only log hunks, kept both sides with 0 lines removed against either parent. All 13 manifest paths match manifest.r2.sha256 (517af41b). Dewey APPROVE c31599 rev 3960; request c31600 revs 3961-3962, posted by Sage because Fethry has no token. Sage gate GREEN 15:54:58Z with its own npm ci and MOSAIC_REQUIRE_EDITOR_BUILD=1: every package suite except queue 0 fail (webui 442, library 149, bus 981, cli 988 plus the allow-listed FED R7 skip, whose runner passed 1/1), all 12 scripts rc 0. The excluded M3 timeout attempt was not counted; the unchanged retry killed cleanly. Dewey N1-N3 non-blocking: the Discord relay must pass check once it calls renderReply; the M3 hang was in the new status test's after-hook teardown; the pre-217 residual stands under sage-3669. #1726 closed c31604. Not re-pinned.
|
||||
|
||||
2026-10-24T16:12:35Z | Sage | Row 287 (#1792, C2-21, Steelbeak on Codex, Boyd and Dewey) LANDED 0e935776e. Before: refactor at 9b6345466. After: Steelbeak's c2338ed58 and doc follow-up a5370b198 cherry-picked onto 9b6345466. One commit equal by range-diff, the other differs only in log hunks, kept both sides. 56 manifest paths match 3683b40a. Boyd APPROVE c31602 rev 3967, Dewey APPROVE c31603 rev 3968; request c31601 revs 3965-3966 posted by Sage. Sage gate GREEN 16:11:38Z: bus 998 (Gitea Docker F-A/F-B and X-G3, minted 4, hits 0), business 283, webui 442, cli 989 plus the allowed FED R7 skip, every other suite 0 fail, all 12 scripts rc 0; fixture credential values in 0 of 29 run files with a positive control. Rulings applied: sage-4125 (library grants mint declared scopes, chief read), mos-1590 (new-test fixture fixes are the lead's call when no production code or old test changes and every fail-closed refusal keeps its own test), sage-4139 (fixture fixes) and sage-4141 (test-land-work.sh is mode 100644, run via bash). Follow-up: F-C negative, a protected Story/** rule refuses the owner too; binds the first row that needs owner pushes to a protected branch. Dewey N1-N3 non-blocking: the block's last line "can write every repository this user can write" contradicts the chief's read grant; a collaborator entry outlives a failed step 5, which still fails closed; #publishFacts would accept a library project, unreachable today. Closes Dewey's 286 r2 N2 (T7/T9 real mint). #1792 closed c31606. Not re-pinned.
|
||||
|
||||
- 2026-10-24 — steelbeak — #1728 / row227 before: transplanted Gizmoduck's unrun C2-14 driver and packet onto refactor487500a16 (471717c59/8039fc1a9). Recovered one-driver/sync-retry/read-only-store choices; replacing no-launcher stand-in with landed row217 harness. Landed-row skips need activation; row288 write/revert/save dependency remains open. Static only under sage-4166; no Node/npm/tests before CLEAR. Notes: agents/steelbeak/work/queue-227/BUILD.md.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Row 227, Case 2 C2-14: end-to-end acceptance on a scratch root (#1728)
|
||||
|
||||
Owner Gizmoduck (sage-3581, 10-23), reviewer Dewey, who also runs the script.
|
||||
Brief: `docs/plans/2026-10-20_case2-rows.md`, section "C2-14 end-to-end acceptance on a scratch
|
||||
root", read at 45dca69f6. Branch `gizmoduck/row227` on 45dca69f6.
|
||||
|
||||
## Files
|
||||
|
||||
- `scripts/test-case2.sh` (new). The entry point: syntax check, one driver run, one counted line per
|
||||
step, an order check on the step ids, and a check that the made-up token is in no driver output.
|
||||
- `scripts/case2/e2e.mjs` (new), its fixture. Every step runs in one node process, because her remote
|
||||
is row 199's fake Gitea over TLS with a scratch CA, and it lives only as long as the process that
|
||||
started it. `checkMapping` takes `https://` remotes only (`packages/library/src/mapping.mjs:46-48`),
|
||||
so a plain file remote isn't an option.
|
||||
|
||||
The driver reuses the test fixtures already on refactor and adds none of its own:
|
||||
`packages/seat/tests/fake-gitea.mjs` (`scratchCa`, `fakeGitea`) and `packages/library/tests/helpers.mjs`
|
||||
(`BASE`, her layout's tree, `MARKER`, `git`, `mappingEntry`).
|
||||
|
||||
## Two stand-ins
|
||||
|
||||
1. **The human.** `human-cli.mjs` proves a human shell and refuses any caller with an agent among its
|
||||
ancestors (`packages/bus/src/human.mjs`; `packages/cli/tests/access.test.mjs:3-4,285-286` pins the
|
||||
refusal). Dewey and I both run under an agent, so a shell driver of human-cli would refuse every
|
||||
human verb. The driver binds the human in-process with `broker.bindHuman`, as the bus tests do
|
||||
(`packages/bus/tests/library-sync.test.mjs:27`). The bus checks after the bind are the real ones.
|
||||
2. **The fake harness.** No launcher and no model. The driver binds the creative instance's run on her
|
||||
thread (`bindLaunch` with `thread`), claims it, receives her message and answers in reply to it.
|
||||
That covers the thread routing and the companion's messaging lists (`roles/companion.json`), and
|
||||
leaves the launcher out. If Dewey wants the launcher in the path, `fakeHarnesses`
|
||||
(`packages/cli/tests/helpers.mjs:133`) is the way in. That is a bigger fixture, and I'd rather take
|
||||
it with C2-06's own fake harness (brief C2-06) than build a second one here.
|
||||
|
||||
## Steps
|
||||
|
||||
The ids are the driver's. "Waits on" names every row whose code the step needs and that isn't on
|
||||
refactor at 45dca69f6. A waiting step prints `skip <id> waits on row <ids>: <text>`, counted apart from
|
||||
pass and fail. When its rows land, the step gets written and its skip line goes.
|
||||
|
||||
| Step | Brief text | State |
|
||||
| --- | --- | --- |
|
||||
| S1a | bare repo in her layout | runs: classify on the fixture mapping (canon, manuscript, playground, unmapped, second universe) |
|
||||
| S1b | with a supporting folder | waits on 283 (C2-01d, supporting folders and the editor field) |
|
||||
| S2a | it syncs | runs: `library.sync` over https from the fake Gitea, served is her head |
|
||||
| S2b | | runs: `library.file` reads canon and playground at served, refuses an unmapped note `project-read-path` |
|
||||
| S2c | | runs: her fast-forward push is served by the next sync |
|
||||
| S2d | | runs: the token is under no file of the data root, the mirror included |
|
||||
| S3a | asks a creative agent in its thread | runs: project, thread on instance `bean` (companion), `thread.send` |
|
||||
| S3b | through a fake harness | runs: the fake harness receives it and its reply is stored in her thread, `in_reply_to` her message |
|
||||
| S3c | stores a reply whose cites verify | waits on 216 (C2-05a lookups) and 217 (C2-06 guard and page store) |
|
||||
| S4 | refuses a playground cite with `"as": "canon"` | waits on 217 (`playground-cited-as-canon`) |
|
||||
| S5a | holds a force-push as `library-diverged` | runs: refused, served unchanged, `diverged.commit` is the forced tip, the refusal recorded |
|
||||
| S5b | | runs: `accept` of a stale commit refuses `library-accept-stale`, `accept` of the tip serves it |
|
||||
| S6 | fixture-grant write, one commit with the trailer, notice in her thread | waits on 283, 285, 286, 288 |
|
||||
| S7 | story-path write refuses `library-write-story-path`, ref unchanged | waits on 283, 288 |
|
||||
| S8 | her revert lands as one commit | waits on 288 |
|
||||
| S9 | her `library.file.save` lands as one commit, her `editor` as author | waits on 283, 288 |
|
||||
| S10 | the Console renders the reply and the notice | waits on 220, 225 |
|
||||
|
||||
Row notes:
|
||||
|
||||
- Sage's in-flight list was 216, 217, 220, 225, 226, 283, 285 and 286. Rows 284, 287, 288 and 289 also
|
||||
touch these steps, and the brief's "After" names the Q226 rows (told Sage in gizmoduck-0419). I mark
|
||||
a step with the rows its code needs, not every row that touches it:
|
||||
- 288 (C2-02) is the write verb, the notice, the revert and `library.file.save`, so S6-S9 all name it.
|
||||
- 287 (C2-21, the grant executor) isn't named on S6, because the brief asks for a fixture grant.
|
||||
- 284 (C2-05c) adds supporting docs to the lookups. S3c cites canon, so it waits on 216 and 217 only.
|
||||
- 289 (C2-11b) is the Console library page. S9 is the verb, which 288 owns. If Dewey reads S9 as the
|
||||
page, 289 joins its list.
|
||||
- 226 (C2-13, install script and precheck) gates no step. A scratch root isn't an install.
|
||||
- PRD 1.17 (row 308) made story files DOCX. The brief predates it and the fixture tree is Markdown.
|
||||
S1-S5 read that tree, which is what the landed reader takes. S6-S9 take the file shape 288 lands with.
|
||||
|
||||
## Choices
|
||||
|
||||
- One driver process, not one per step: the fake Gitea's port and CA would change with each process,
|
||||
and the library entry's remote is fixed at bus start.
|
||||
- The bus timer stays on with a one-hour period, since `startBroker` needs `librarySync` to carry the
|
||||
CA's `command`. Its pass at start can hold the mirror, so a step's sync retries
|
||||
`library-sync-busy` for up to about 10 s.
|
||||
- The refusal check opens the bus store read-only (`node:sqlite`, `readOnly: true`). The runtime
|
||||
hands back no store, and no bus verb lists the events.
|
||||
- Git in the driver runs with `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_CONFIG_NOSYSTEM=1`, so a caller's
|
||||
hooks, signing or credential helper can't reach it. `helpers.mjs` `git` already passes the
|
||||
identity and `core.hooksPath=/dev/null`.
|
||||
|
||||
## Carry for Dewey
|
||||
|
||||
- The launcher in the path waits for C2-06's fake harness (row 217). Sage accepted both stand-ins,
|
||||
labelled, in sage-3608. When 217 lands, S3b moves to its fake harness behind the launcher.
|
||||
|
||||
## Not yet run
|
||||
|
||||
Darkwing's re-pin window holds the slot (sage-3581), so as of this draft no node ran on it: no
|
||||
`node --check`, no driver run, no suite. `bash -n scripts/test-case2.sh` passes. The first run waits for
|
||||
Sage's CLEAR, and the runs go below.
|
||||
|
||||
## Runs
|
||||
@@ -0,0 +1,114 @@
|
||||
# Row 227 C2-14 (#1728): Steelbeak rebuild
|
||||
|
||||
Assigned by Sage (sage-4166, 2026-10-24), reviewer Dewey. Worktree
|
||||
/home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row227, branch steelbeak/row227,
|
||||
base refactor 487500a16f4ab5902c318509fdec6acc56a543ca.
|
||||
|
||||
## Recovered packet and choices
|
||||
|
||||
Gizmoduck's packet is preserved, unchanged, under agents/gizmoduck/work/queue-227.
|
||||
Cherry-picks: 2d1efa0de -> 471717c59 and 5583fa775 -> 8039fc1a9. Both clean.
|
||||
Read Choices first: one driver process owns fake TLS Gitea lifetime, the runtime's startup
|
||||
sync can require library-sync-busy retries, and refusal evidence uses a read-only store handle.
|
||||
Human bind remains an explicitly labelled in-process stand-in. The no-launcher stand-in
|
||||
comes out under sage-3608 now that row217 is landed: reuse its scripted citation adapter
|
||||
through the real launcher and fakeHarnesses module loader.
|
||||
|
||||
## Authority and state
|
||||
|
||||
Static edits, local git and own-branch pushes are authorized. NO Node invocation (including
|
||||
node --check), npm, driver run, suites or mutants before Sage's CLEAR. Row257 owns the slot;
|
||||
266/224/214 precede this row. No code in this packet has been executed by this seat.
|
||||
|
||||
## Current dependency delta
|
||||
|
||||
Canonical queue inspection: 216/217/220/225/226/283/285/286/287 are done. Row288 remains
|
||||
briefed (Bubba); row289 remains briefed. The runtime has no library.write, library.revert or
|
||||
library.file.save implementation yet, so S6-S9 and S10's real write notice remain dependent
|
||||
on288. Reported to Sage in steelbeak-0020 with a recommendation to keep pending steps
|
||||
explicit and never count them as complete acceptance. Independent landed-step rebuild proceeds.
|
||||
|
||||
## Planned static work
|
||||
|
||||
1. Replace manual bindLaunch S3b with the existing scripted citation adapter behind createLauncher.
|
||||
2. Activate supporting mapping/editor, real lookup/citation guard/storage and playground-canon refusal.
|
||||
3. Adapt runtime/config/credential fixture to current roles while keeping all state scratch-only.
|
||||
4. Add the Console render path for checked replies; write/revert/save/notice follow landed APIs only.
|
||||
5. Shorten fixed fixture credentials to under24 characters; ensure output diagnostics never contain one.
|
||||
6. Search packages/*/tests and scripts for hash pins on each changed path; record searches and controls.
|
||||
7. Prepare mutant and full-script gate plan, then push a plainly UNTESTED packet and manifest for CLEAR.
|
||||
|
||||
## Runs
|
||||
|
||||
None. Static reads and git operations only.
|
||||
|
||||
## Sage ruling sage-4167
|
||||
|
||||
No slot request until row288 lands. Build landed steps now; draft S6-S9 and S10 notice against
|
||||
row288's brief, but no stub, skip or acceptance count. After224 ->284 ->288 lands, rebase and
|
||||
complete S6-S10, then request one slot for one full acceptance run with nothing skipped.
|
||||
Row already in-progress at3984; do not retry the refused queue-start or touch its lock.
|
||||
|
||||
| Pending step | Row288 verb/evidence | State |
|
||||
| --- | --- | --- |
|
||||
| S6 | library.doc.write; one seat-authored parented commit, trailer and broker notice | drafted, unrun |
|
||||
| S7 | library.doc.write story refusal library-write-story-path; refs unchanged | drafted, unrun |
|
||||
| S8 | library.revert {project,commit}, A2 sage-3596; editor-authored revert commit | drafted, unrun |
|
||||
| S9 | library.file.save {path,base,content}; editor-authored save commit | drafted, unrun |
|
||||
| S10 notice | actual broker library-write message from S6 through Console HTTP and DOM | drafted, unrun |
|
||||
|
||||
The source contains unconditional implementations of all17 steps and no skip writer. None is
|
||||
counted toward acceptance before the gated full run. S6-S10 signatures and runtime wiring must be
|
||||
reconciled with actual288 landing before requesting that run.
|
||||
|
||||
## Static rebuild so far
|
||||
|
||||
New fixture modules scripts/case2/launch.mjs and console.mjs keep the one-driver lifecycle.
|
||||
The launcher uses existing CLI fixture config, real bootConfig/startBroker/createLauncher and
|
||||
C2-06 citation-adapter via an adapter shell wrapper. Only a fixture grant is seeded, as the brief
|
||||
requests; no executor mint is substituted into row227. Actual launcher bind results are captured
|
||||
for later seat requests; no manual bindLaunch stand-in remains. Secret store files are outside
|
||||
data root, with distinct short fetch/seat values. The real message guard supplies stored checks;
|
||||
refusal/event reads remain read-only. The Console path uses consoleBus and startServer with
|
||||
actual chat rows, plus repository browser helper; only unrelated Board service is a local503.
|
||||
|
||||
Checks performed: bash -n scripts/test-case2.sh passed; git diff --check passed. Neither parses
|
||||
or executes JavaScript. NO Node/npm/driver/suites/mutants have run.
|
||||
|
||||
## Static preparation receipt
|
||||
|
||||
- Preserved Gizmoduck packet byte-for-byte against origin/gizmoduck/row227.
|
||||
- bash -n scripts/test-case2.sh: rc0. JavaScript syntax has NOT been checked.
|
||||
- git diff --check: rc0 on current edits.
|
||||
- static/hash-pin-search.txt records literal-path searches across packages/*/tests and scripts for
|
||||
every changed source/note path. No path-specific hash pins; only the driver self-reference and
|
||||
BUILD-LOG.md's gate-lane copy-exclusion entry. case1-accept.sh references are the positive control.
|
||||
- static/source-review.txt records the17 step ids in order and absence of a skip writer. This is
|
||||
source inspection, NOT an execution or an acceptance count.
|
||||
- No Node, npm, driver run, suite, browser, launcher or mutant has run. All helper code is UNTESTED.
|
||||
|
||||
Pre-CLEAR manifest is preparatory, not a frozen review candidate or a slot request. Static commits
|
||||
and pushes are explicitly authorized by sage-4166. No test or acceptance success is claimed.
|
||||
|
||||
## Completion after row288 lands
|
||||
|
||||
1. Rebase onto its refactor landing, retaining the two original cherry-pick receipts and packet.
|
||||
2. Reconcile224/284 lookup/tool snapshots and288 write/revert/save interfaces against the actual code.
|
||||
Current S8 follows A2's {project,commit}; current S9 follows the brief's {path,base,content}.
|
||||
3. Finish/verify S6-S10's real write, story refusal, revert, save and actual Console notice path.
|
||||
4. Send one slot request to Sage for the whole script, as sage-4167 requires; wait for CLEAR.
|
||||
5. Under CLEAR install pinned local dependencies in both worktree root and packages/webui with
|
||||
npm ci --ignore-scripts --no-audit --no-fund; use a new short mktemp TMPDIR. No canonical npm/linking.
|
||||
6. Execute the full script once with all17 steps and zero skips. Any first-run new-test RED is
|
||||
placed/reported; an old-test/product/loosened assertion or subsequent RED goes to Sage first.
|
||||
7. Proposed semantic negatives: disable playground-canon check ->S4; serve force-pushed tip ->S5;
|
||||
remove supporting-path enforcement ->S7; omit agent trailer/notice ->S6/S10; wrong editor ->S8/S9;
|
||||
remove stored guard metadata ->S3c/S10. A timeout is never a mutant kill. Restore byte-for-byte.
|
||||
8. Run applicable library/bus/harness/CLI/webui suites and every scripts/test-*.sh using bash with
|
||||
timeout -k10; MOSAIC_REQUIRE_EDITOR_BUILD=1 for webui. FED skip needs its dedicated same-gate runner.
|
||||
Redo hash-pin search after reconciliation, inspect token/output and owned-process cleanup receipts.
|
||||
9. Freeze final tested manifest and hand off directly to Dewey/Sage. Dewey runs the script; Sage's
|
||||
landing gate must also pass. No live host or model, no role expansion, no merge/deploy authorized.
|
||||
|
||||
The sole present boundary is row288's unlanded dependency plus explicit no-execution/no-slot ruling;
|
||||
no Jason decision or secret is requested. Leave row227 in-progress, as instructed.
|
||||
@@ -0,0 +1,10 @@
|
||||
3e78b4cbabc028295045095c2538d0ea805680cb686a2b48f468c7dc3a606ca1 BUILD-LOG.md
|
||||
a29f02b908b09cdcf61a7ac4090665206c0527f8c4b90c0bf0b1100f7d4bcb4c agents/gizmoduck/work/queue-227/BUILD.md
|
||||
68b1e4cbeac27181368d9ff2d2f1a1ff3884cbcdbe0b8e0e4b48d25385fa6d58 agents/steelbeak/work/queue-227/BUILD.md
|
||||
0b5b7ce94a4e36f748cc85031929aa664012a97683483297724670f94ca0e29a agents/steelbeak/work/queue-227/static/hash-pin-search.txt
|
||||
a888b2fa6a9b4eeaecfea58639e46e3597bd7edf4f65ea6d5bb4e3c4032debd9 agents/steelbeak/work/queue-227/static/source-review.txt
|
||||
f8ed4a3d100309250aaf4e1a50b7787ce176ef7f2941a23c114dc8f7ed5e8e2e docs/SESSIONS.md
|
||||
5fb4eba80ace7a64959f2fefbea8c4521d0054bbe348df70c0ecb694c2e5ed65 scripts/case2/console.mjs
|
||||
9ff32edf8af389868f92d502a7a7d2ff65e77174dc7523dd22e27919ce94ce90 scripts/case2/e2e.mjs
|
||||
2e9d27ba06f968fd4320cad7243131bca57f4945075878d5d868850c514dc6de scripts/case2/launch.mjs
|
||||
f903c1de232314c9e0795ce1c8c3a8ff3bb38c91bc46e6405e55e6ef3f1969a4 scripts/test-case2.sh
|
||||
@@ -0,0 +1,18 @@
|
||||
Static hash-pin search, no Node execution.
|
||||
Searched every changed path in packages/*/tests and scripts.
|
||||
scripts/test-case2.sh: rg rc 1
|
||||
scripts/case2/e2e.mjs: rg rc 0
|
||||
scripts/case2/e2e.mjs:245:if (!root) { process.stderr.write('usage: node scripts/case2/e2e.mjs <scratch dir>\n'); process.exit(2); }
|
||||
scripts/test-case2.sh:33:DRIVER=scripts/case2/e2e.mjs
|
||||
scripts/case2/launch.mjs: rg rc 1
|
||||
scripts/case2/console.mjs: rg rc 1
|
||||
agents/gizmoduck/work/queue-227/BUILD.md: rg rc 1
|
||||
agents/steelbeak/work/queue-227/BUILD.md: rg rc 1
|
||||
BUILD-LOG.md: rg rc 0
|
||||
packages/cli/tests/gate-lane-auth.test.mjs:49:const SKIP_COPY = new Set(['.git', 'node_modules', 'v1', 'agents', 'docs', 'BUILD-LOG.md', join('scripts', 'foundation')]);
|
||||
docs/SESSIONS.md: rg rc 1
|
||||
Positive control: existing case1-accept.sh references (first three)
|
||||
1:// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
|
||||
19:const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
|
||||
755: "retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
|
||||
Conclusion: only new self-reference and gate-lane copy-exclusion references; no path-specific hash pins on changed files.
|
||||
@@ -0,0 +1,6 @@
|
||||
Static source inventory only; no JavaScript parse or execution.
|
||||
Declared step ids: S1a S1b S2a S2b S2c S2d S3a S3b S3c S4 S5a S5b S6 S7 S8 S9 S10
|
||||
Step ids match required order: yes
|
||||
Skip writer/call in driver: absent
|
||||
Pending S6-S9/S10 notice are unconditional future-interface drafts, not acceptance passes.
|
||||
Preserved human bind stand-in; creative session uses actual launcher and row217 adapter.
|
||||
@@ -1678,3 +1678,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
- 2026-10-24 — steelbeak — row 287 C2-21 (#1792), builder complete under sage-4130: collaborator library grants and chief real read mint built; bus 998/business 283/CLI 989+FED1/Docker2/install27/all12 shell gates green, M1-M11 killed/restored, token/cleanup audits clean; preparing steelbeak/row287 push and manifest handoff to Boyd then Dewey, no live change or landing.
|
||||
- 2026-10-24 — steelbeak — row 287 first-review follow-up: Boyd approved c2338ed58/3683b40... with two doc-only corrections; append-only BUILD correction distinguishes clean product/test diff from raw-log whitespace and records old-test numstats. Queue round1 open; missing-seat-credential request failure reported to Sage. Frozen product and manifest preserved; no tests or slot use.
|
||||
2026-10-24T16:12:35Z | Sage (1ef1e4f8) | Rows 225 and 287 landed (mos-1576, mos-1590) | 225 9b6345466 (Dewey c31599, gate GREEN 15:54:58Z, #1726 closed c31604); 287 0e935776e (Boyd c31602, Dewey c31603, gate GREEN 16:11:38Z, #1792 closed c31606); F-C owner-push follow-up and Dewey N1-N3 recorded in BUILD-LOG; no re-pin.
|
||||
- 2026-10-24 — steelbeak — row227 C2-14 (#1728), sage-4166: new worktree steelbeak-row227 on refactor487500a16, Gizmoduck commits transplanted cleanly to471717c59/8039fc1a9, packet preserved; static rebuild underway, no Node/npm/tests before CLEAR, remaining row288 dependency reported.
|
||||
- 2026-10-24 — steelbeak — row227 static packet: real-launcher C2-06 adapter and checked-reply/Console path drafted, all17 source steps unconditional; S6-S9/S10 notice pending on288 under sage-4167. Shell syntax/whitespace and hash-pin searches only; no Node/npm/tests/browser/launcher/mutants. Preparing clearly UNTESTED seat-branch push; no slot request until288 lands.
|
||||
@@ -0,0 +1,43 @@
|
||||
// C2-14 S10: actual Console HTTP chat endpoints and browser DOM over the same acceptance bus.
|
||||
// The unrelated Board is a local 503 fixture. No chat response, citation check or notice is stubbed.
|
||||
import { createServer } from 'node:http';
|
||||
import { startServer } from '../../packages/webui/src/serve.mjs';
|
||||
import { consoleBus } from '../../packages/webui/src/console-bus.mjs';
|
||||
import { browser } from '../../packages/webui/tests/browser.mjs';
|
||||
|
||||
const shutdown = server => new Promise((resolve, reject) => {
|
||||
server.close(error => error ? reject(error) : resolve());
|
||||
server.closeAllConnections?.();
|
||||
});
|
||||
const expect = (value, label) => { if (!value) throw new Error(label); };
|
||||
export async function consoleAcceptance({ humanCall, thread, reply, writeNotice, writeCommit, cleanups }) {
|
||||
const board = createServer((req, res) => { res.writeHead(503); res.end('Acceptance Board is not configured.'); });
|
||||
await new Promise(resolve => board.listen(0, '127.0.0.1', resolve));
|
||||
cleanups.push(() => shutdown(board));
|
||||
const options = consoleBus(humanCall);
|
||||
const server = await startServer({ port: 0, board: `http://127.0.0.1:${board.address().port}`, ...options, log: () => {} });
|
||||
cleanups.push(() => shutdown(server));
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
const response = await fetch(`${origin}/api/chat/threads/${thread}/messages`);
|
||||
expect(response.ok, 'Console message endpoint refused');
|
||||
const body = await response.json();
|
||||
expect(Array.isArray(body.rows), 'Console message shape');
|
||||
const checked = body.rows.find(m => m.id === reply.id), notice = body.rows.find(m => m.id === writeNotice.id);
|
||||
expect(checked?.body === reply.body && checked.check?.citations?.[0]?.scope === 'canon', 'Console dropped stored reply/check');
|
||||
expect(notice?.body === writeNotice.body, 'Console dropped broker notice');
|
||||
const view = await browser({ machine: false });
|
||||
cleanups.push(() => view.close());
|
||||
await view.navigate(`${origin}/#/chat/threads/${thread}`);
|
||||
const rendered = await view.evaluate(`(async () => {
|
||||
for (let i = 0; i < 150; i++) {
|
||||
const bodies = [...document.querySelectorAll('.chat-log .chat-body')].map(e => e.textContent);
|
||||
const notices = [...document.querySelectorAll('.chat-notice > span:first-child')].map(e => e.textContent);
|
||||
const canon = [...document.querySelectorAll('.chat-cites')].some(e => e.textContent.includes('canon'));
|
||||
if (bodies.some(t => t === 'Grey eyes.') && canon && notices.some(t => t === ${JSON.stringify(`bean wrote ${JSON.parse(writeNotice.body).path} @${writeCommit.slice(0, 7)}`)}))
|
||||
return { reply: true, canon, notice: true };
|
||||
await new Promise(resolve => setTimeout(resolve, 40));
|
||||
}
|
||||
return { reply: false, canon: false, notice: false };
|
||||
})()`);
|
||||
expect(rendered.reply && rendered.canon && rendered.notice, 'Console did not render actual checked reply and write notice');
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// C2-14 (#1728): one scratch-root acceptance driver. No live host or real model.
|
||||
// The human uses bindHuman in-process because a development agent cannot prove human CLI ancestry.
|
||||
// The creative session is real createLauncher -> runner -> C2-06's scripted citation adapter.
|
||||
// S6-S10 are drafted against row288's brief; do not run/request a slot until288 lands (sage-4167).
|
||||
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { scratchCa, fakeGitea } from '../../packages/seat/tests/fake-gitea.mjs';
|
||||
import { BASE, MARKER, git, mappingEntry } from '../../packages/library/tests/helpers.mjs';
|
||||
import * as library from '../../packages/library/src/index.mjs';
|
||||
import { launchFixture } from './launch.mjs';
|
||||
import { consoleAcceptance } from './console.mjs';
|
||||
|
||||
const FETCH_TOKEN = 'fx227-fetch-q7';
|
||||
const SEAT_TOKEN = 'fx227-seat-q7';
|
||||
const USER = 'books-bot', BUSINESS = 'acme';
|
||||
const CHARACTER = 'Book One/Characters/Mara Vell.md';
|
||||
const SUPPORT = 'Book One/Notes/Eyes.md';
|
||||
const WRITE = 'Book One/Notes/Agent.md';
|
||||
const EDITOR = { name: 'Fixture author', email: '[email protected]' };
|
||||
const secrets = [FETCH_TOKEN, SEAT_TOKEN];
|
||||
const redact = value => secrets.reduce((s, token) => s.replaceAll(token, '[fixture credential]'), String(value));
|
||||
let failures = 0;
|
||||
const out = line => process.stdout.write(redact(line) + '\n');
|
||||
const expect = (cond, message) => { if (!cond) throw new Error(message); };
|
||||
async function step(id, text, fn) {
|
||||
try { await fn(); out(`ok ${id} ${text}`); }
|
||||
catch (e) { failures++; out(`not ok ${id} ${text}: ${e?.code ?? ''} ${String(e?.message ?? e).split('\n')[0].slice(0, 300)}`.trimEnd()); }
|
||||
}
|
||||
async function refuses(promise, code) {
|
||||
try { await promise; }
|
||||
catch (e) { if (e?.code === code) return; throw new Error(`refused ${e?.code ?? 'unknown'}, expected ${code}`); }
|
||||
throw new Error(`answered, expected ${code}`);
|
||||
}
|
||||
async function until(fn, label, ms = 20000) {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) { const result = await fn(); if (result) return result; await new Promise(r => setTimeout(r, 30)); }
|
||||
throw new Error(`timed out: ${label}`);
|
||||
}
|
||||
function holdsToken(dir) {
|
||||
for (const name of readdirSync(dir, { recursive: true })) {
|
||||
const file = join(dir, name);
|
||||
let st;
|
||||
try { st = statSync(file); } catch { continue; }
|
||||
if (st.isFile() && secrets.some(value => readFileSync(file).includes(value))) return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function readEvents(dataRoot, where = "kind='action.refused'") {
|
||||
const db = new DatabaseSync(join(dataRoot, 'bus', 'bus.sqlite'), { readOnly: true });
|
||||
try { return db.prepare(`SELECT body FROM events WHERE ${where} ORDER BY seq`).all().map(row => JSON.parse(row.body)); }
|
||||
finally { db.close(); }
|
||||
}
|
||||
|
||||
const cleanups = [];
|
||||
let closing = null;
|
||||
const close = () => (closing ??= (async () => {
|
||||
const errors = [];
|
||||
for (const cleanup of cleanups.reverse()) { try { await cleanup(); } catch { errors.push('cleanup failed'); } }
|
||||
if (errors.length) throw new Error('acceptance resource cleanup failed');
|
||||
})());
|
||||
for (const [name, status] of [['SIGTERM', 143], ['SIGINT', 130]])
|
||||
process.once(name, () => { void close().finally(() => process.exit(status)); });
|
||||
|
||||
async function main(root) {
|
||||
const work = join(root, 'her', 'books');
|
||||
mkdirSync(work, { recursive: true });
|
||||
git(work, 'init', '-q', '-b', 'main');
|
||||
const files = { ...BASE, [SUPPORT]: '# Eye notes\nGrey eyes are in the character record.\n' };
|
||||
for (const [path, text] of Object.entries(files)) {
|
||||
mkdirSync(dirname(join(work, path)), { recursive: true }); writeFileSync(join(work, path), text);
|
||||
}
|
||||
git(work, 'add', '-A'); git(work, 'commit', '-qm', 'her first commit');
|
||||
const first = git(work, 'rev-parse', 'HEAD');
|
||||
const tls = scratchCa(join(root, 'ca'));
|
||||
const gitea = await fakeGitea({ dir: join(root, 'gitea'), tls, user: USER, token: FETCH_TOKEN,
|
||||
users: { [USER]: FETCH_TOKEN, 'acme-bean': SEAT_TOKEN } });
|
||||
cleanups.push(() => gitea.close());
|
||||
const remote = gitea.repo('writer', 'books');
|
||||
mkdirSync(dirname(remote), { recursive: true });
|
||||
git(root, 'clone', '-q', '--bare', '--no-local', work, remote);
|
||||
const push = (changed, { from = null } = {}) => {
|
||||
if (from) git(work, 'reset', '-q', '--hard', from);
|
||||
for (const [path, text] of Object.entries(changed)) {
|
||||
mkdirSync(dirname(join(work, path)), { recursive: true }); writeFileSync(join(work, path), text);
|
||||
}
|
||||
git(work, 'add', '-A'); git(work, 'commit', '-qm', 'her next commit');
|
||||
git(work, 'push', '-q', '--force', remote, 'HEAD:refs/heads/main');
|
||||
return git(work, 'rev-parse', 'HEAD');
|
||||
};
|
||||
const remoteHead = () => git(remote, 'rev-parse', 'refs/heads/main');
|
||||
const entry = { ...mappingEntry(), remote: `${gitea.origin}/writer/books.git`, user: USER, editor: EDITOR };
|
||||
entry.universes.harbor.supporting = ['Book One/Notes/'];
|
||||
const f = await launchFixture(root, entry, { fetchToken: FETCH_TOKEN, seatToken: SEAT_TOKEN, ca: tls.ca });
|
||||
cleanups.push(() => f.close());
|
||||
const { runtime: bus, dataRoot } = f, broker = bus.broker;
|
||||
const human = broker.bindHuman({ business: BUSINESS, human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const call = (cap, verb, args = {}) => Promise.resolve().then(() => broker.request(cap, { verb, args }));
|
||||
const humanCall = (verb, args = {}) => call(human, verb, args);
|
||||
const sync = async (args = {}) => {
|
||||
for (let i = 0; ; i++) {
|
||||
try { return await humanCall('library.sync', { project: 'books', ...args }); }
|
||||
catch (e) { if (e?.code !== 'library-sync-busy' || i >= 100) throw e; await new Promise(r => setTimeout(r, 100)); }
|
||||
}
|
||||
};
|
||||
const status = () => humanCall('library.status', { project: 'books' });
|
||||
const mapping = library.checkMapping(entry);
|
||||
const mirror = library.mirrorPath({ business: f.business, project: 'books', dataRoot });
|
||||
const source = async () => ({ repo: mirror, mapping, commit: (await status()).served });
|
||||
|
||||
await step('S1a', 'her bare layout classifies canon, manuscript, playground, unmapped and second-universe paths', () => {
|
||||
expect(git(remote, 'rev-parse', '--is-bare-repository') === 'true', 'remote is not bare');
|
||||
const of = p => library.classify(mapping, p);
|
||||
expect(of(CHARACTER).scope === 'canon' && of(CHARACTER).type === 'character', 'character');
|
||||
expect(of('Book One/Chapters/Chapter 01.md').type === 'manuscript', 'chapter');
|
||||
expect(of('Book One/Playground/What If.md').scope === 'playground', 'playground');
|
||||
expect(of('Inbox Note.md').scope === 'unmapped', 'inbox');
|
||||
expect(of('Second World/Chapters/01.md').universe === 'second', 'second universe');
|
||||
});
|
||||
await step('S1b', 'a supporting folder and her editor are part of the checked mapping', async () => {
|
||||
expect(library.classify(mapping, SUPPORT).scope === 'supporting', 'supporting scope');
|
||||
expect(library.supportingPath(mapping, 'harbor', WRITE, await library.listTree(remote, first)) === WRITE, 'supporting write path');
|
||||
expect(mapping.editor.name === EDITOR.name && mapping.editor.email === EDITOR.email, 'editor');
|
||||
});
|
||||
await step('S2a', 'library.sync fetches her bare repo and serves its head', async () => {
|
||||
const state = await sync();
|
||||
expect(state.served === first && state.served === remoteHead(), 'wrong served commit');
|
||||
expect(state.lastError === null && state.diverged === null, 'sync error or divergence');
|
||||
});
|
||||
await step('S2b', 'library.file reads canon and playground at served, and refuses an unmapped file', async () => {
|
||||
const canon = await humanCall('library.file', { project: 'books', path: CHARACTER });
|
||||
expect(canon.commit === first && canon.content.includes(MARKER), 'canon file');
|
||||
const play = await humanCall('library.file', { project: 'books', path: 'Book One/Playground/What If.md' });
|
||||
expect(play.content.includes(MARKER), 'playground file');
|
||||
await refuses(humanCall('library.file', { project: 'books', path: 'Inbox Note.md' }), 'project-read-path');
|
||||
});
|
||||
await step('S2c', 'her fast-forward push is served by the next sync', async () => {
|
||||
const next = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nSalt and tar. ${MARKER}\n` });
|
||||
expect((await sync()).served === next, 'fast-forward not served');
|
||||
});
|
||||
await step('S2d', 'fixture credentials occur nowhere under the data root, including the mirror', () => {
|
||||
expect(holdsToken(dataRoot) === null, 'credential found under the data root');
|
||||
});
|
||||
|
||||
let thread, asked, reply, writeCommit, writeNotice;
|
||||
let citedCommit;
|
||||
await step('S3a', 'she makes a creative thread and asks in it', async () => {
|
||||
const project = (await humanCall('project.create', { name: 'books', repo: 'books' })).id;
|
||||
thread = (await humanCall('thread.create', { project, name: 'Book One', instance: 'bean' })).id;
|
||||
const looked = await library.get(await source(), { universe: 'harbor', path: CHARACTER });
|
||||
expect(looked && looked.scope === 'canon', 'lookup missing');
|
||||
const index = looked.lines.findIndex(line => line.includes('Grey eyes.'));
|
||||
expect(index >= 0, 'lookup quote missing');
|
||||
citedCommit = looked.commit;
|
||||
const cite = { path: CHARACTER, commit: citedCommit, lines: [index + 1, index + 1], quote: 'Grey eyes.', as: 'canon' };
|
||||
const valid = text => JSON.stringify({ text, citations: [cite] });
|
||||
const bad = JSON.stringify({ text: 'Playground is canon.', citations: [{ path: 'Book One/Playground/What If.md',
|
||||
commit: citedCommit, lines: [4, 4], quote: MARKER, as: 'canon' }] });
|
||||
f.setPlan([{ text: valid('Grey eyes.') }, { text: bad }, { text: valid('The playground cannot establish canon.') }]);
|
||||
asked = (await humanCall('thread.send', { thread, body: "What colour are Mara Vell's eyes?" })).id;
|
||||
});
|
||||
await step('S3b', 'the real launcher runs the C2-06 fake harness and stores its reply in her thread', async () => {
|
||||
reply = await until(async () => (await humanCall('thread.messages', { thread })).find(m => m.in_reply_to === asked && m.class === 'RESULT'), 'launcher reply');
|
||||
expect(reply.from_role === 'bean' && f.cap(thread), 'reply did not use managed bean run');
|
||||
const record = f.records.find(r => r.thread === thread);
|
||||
expect(record && Number.isSafeInteger(record.pid) && record.pid > 1, 'no launched process record');
|
||||
const runDir = dirname(join(dataRoot, 'launches', BUSINESS, record.run, 'session.json'));
|
||||
expect(Number(readFileSync(join(runDir, 'fixture-count'), 'utf8')) === 1, 'scripted adapter was not called exactly once');
|
||||
});
|
||||
await step('S3c', 'her stored reply carries a verified lookup citation at the served commit', () => {
|
||||
const body = JSON.parse(reply.body);
|
||||
expect(body.text === 'Grey eyes.' && body.citations[0].commit === citedCommit, 'wrong reply');
|
||||
expect(reply.check?.citations?.length === 1 && reply.check.citations[0].scope === 'canon', 'missing stored guard check');
|
||||
const events = readEvents(dataRoot, "kind='action.allowed'");
|
||||
expect(events.some(e => e.action === 'message.checked' && e.message === reply.id && e.check.citations[0].scope === 'canon'), 'no stored check event');
|
||||
});
|
||||
await step('S4', 'a playground-as-canon reply refuses, and the runner retries without storing the bad body', async () => {
|
||||
const next = (await humanCall('thread.send', { thread, body: 'Can the playground establish canon?' })).id;
|
||||
const second = await until(async () => (await humanCall('thread.messages', { thread })).find(m => m.in_reply_to === next && m.class === 'RESULT'), 'guard retry');
|
||||
expect(JSON.parse(second.body).text === 'The playground cannot establish canon.', 'retry answer');
|
||||
const history = await humanCall('thread.messages', { thread });
|
||||
expect(!history.some(m => m.body.includes('Playground is canon.')), 'refused reply persisted');
|
||||
expect(readEvents(dataRoot).some(e => e.code === 'playground-cited-as-canon'), 'missing refusal');
|
||||
const record = f.records.find(r => r.thread === thread);
|
||||
expect(Number(readFileSync(join(dataRoot, 'launches', BUSINESS, record.run, 'fixture-count'), 'utf8')) === 3, 'retry count');
|
||||
});
|
||||
await step('S5a', 'her force-push holds library-diverged without moving served', async () => {
|
||||
const before = (await status()).served;
|
||||
const forced = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nRewritten. ${MARKER}\n` }, { from: first });
|
||||
await refuses(sync(), 'library-diverged');
|
||||
const state = await status();
|
||||
expect(state.served === before && state.diverged?.commit === forced, 'force-push was not held');
|
||||
expect(readEvents(dataRoot).some(e => e.code === 'library-diverged'), 'missing divergence refusal');
|
||||
});
|
||||
await step('S5b', 'a stale accept refuses, then her accepted tip becomes served', async () => {
|
||||
await refuses(sync({ accept: first }), 'library-accept-stale');
|
||||
const tip = remoteHead(), state = await sync({ accept: tip });
|
||||
expect(state.served === tip && state.diverged === null, 'tip not accepted');
|
||||
});
|
||||
|
||||
// Pending on row288, not stubs or accepted skips. These calls become runnable only after its landing.
|
||||
await step('S6', 'the fixture-granted seat writes one supporting commit with its trailer and a thread notice', async () => {
|
||||
const before = remoteHead();
|
||||
await call(f.cap(thread), 'library.doc.write', { universe: 'harbor', path: WRITE, base: null, content: '# Agent note\nA supporting observation.\n' });
|
||||
writeCommit = remoteHead();
|
||||
expect(writeCommit !== before && git(remote, 'rev-list', '--count', `${before}..${writeCommit}`) === '1', 'write must add exactly one commit');
|
||||
expect(git(remote, 'show', '-s', '--format=%P', writeCommit) === before, 'write parent');
|
||||
expect(git(remote, 'show', '-s', '--format=%B', writeCommit).includes('Mosaic-Agent: acme/bean'), 'missing agent trailer');
|
||||
expect(git(remote, 'show', '-s', '--format=%an <%ae>', writeCommit) === 'Bean fixture <[email protected]>', 'seat author');
|
||||
expect(gitea.requests.some(r => r.method === 'POST' && r.user === 'acme-bean' && r.path.endsWith('/git-receive-pack')), 'seat credential not used for push');
|
||||
writeNotice = (await humanCall('thread.messages', { thread })).find(m => {
|
||||
try { const body = JSON.parse(m.body); return body.notice === 'library-write' && body.commit === writeCommit; } catch { return false; }
|
||||
});
|
||||
expect(writeNotice?.from_role === 'bean', 'notice missing from her thread');
|
||||
const noticeBody = JSON.parse(writeNotice.body);
|
||||
expect(Object.keys(noticeBody).sort().join(',') === 'commit,notice,op,path' && noticeBody.notice === 'library-write'
|
||||
&& noticeBody.op === 'write' && noticeBody.path === WRITE && noticeBody.commit === writeCommit, 'notice body');
|
||||
});
|
||||
await step('S7', 'a story-path write refuses library-write-story-path and leaves her remote unchanged', async () => {
|
||||
const before = remoteHead(), served = (await status()).served;
|
||||
await refuses(call(f.cap(thread), 'library.doc.write', { universe: 'harbor', path: 'Book One/Chapters/Chapter 01.md', base: null, content: 'Unwanted story edit.\n' }), 'library-write-story-path');
|
||||
expect(remoteHead() === before && (await status()).served === served, 'refusal moved a ref');
|
||||
});
|
||||
await step('S8', 'her revert of the agent write adds exactly one editor-authored commit', async () => {
|
||||
const before = remoteHead();
|
||||
// A2 (sage-3596): revert names the business project key as well as the commit.
|
||||
const result = await humanCall('library.revert', { project: 'books', commit: writeCommit });
|
||||
const after = remoteHead();
|
||||
expect(after !== before && git(remote, 'rev-list', '--count', `${before}..${after}`) === '1', 'revert commit count');
|
||||
expect(git(remote, 'show', '-s', '--format=%P', after) === before, 'revert must have exactly one parent');
|
||||
expect(result.reverted === writeCommit && result.commit === after, 'revert result');
|
||||
expect(!git(remote, 'ls-tree', '-r', '--name-only', after).split('\n').includes(WRITE), 'revert did not remove the newly created note');
|
||||
expect(git(remote, 'show', '-s', '--format=%an <%ae>', after) === `${EDITOR.name} <${EDITOR.email}>`, 'revert editor');
|
||||
});
|
||||
await step('S9', 'her library.file.save adds one commit with her editor as author', async () => {
|
||||
const before = remoteHead(), base = git(remote, 'rev-parse', `${before}:${CHARACTER}`);
|
||||
const content = git(remote, 'show', `${before}:${CHARACTER}`) + '\nHer saved note.\n';
|
||||
await humanCall('library.file.save', { path: CHARACTER, base, content });
|
||||
const after = remoteHead();
|
||||
expect(after !== before && git(remote, 'rev-list', '--count', `${before}..${after}`) === '1', 'save commit count');
|
||||
expect(git(remote, 'show', '-s', '--format=%P', after) === before, 'save must have exactly one parent');
|
||||
expect(git(remote, 'show', '-s', '--format=%an <%ae>', after) === `${EDITOR.name} <${EDITOR.email}>`, 'save editor');
|
||||
expect(git(remote, 'show', `${after}:${CHARACTER}`).endsWith('Her saved note.'), 'save content');
|
||||
});
|
||||
await step('S10', 'the real Console renders her checked reply and the broker write notice', async () => {
|
||||
expect(writeNotice && writeCommit, 'row288 write notice is required');
|
||||
await consoleAcceptance({ humanCall, thread, reply, writeNotice, writeCommit, cleanups });
|
||||
});
|
||||
expect(holdsToken(dataRoot) === null, 'credential persisted during the later steps');
|
||||
}
|
||||
|
||||
const root = process.argv[2];
|
||||
if (!root) { process.stderr.write('usage: node scripts/case2/e2e.mjs <scratch dir>\n'); process.exit(2); }
|
||||
try { await main(resolve(root)); }
|
||||
catch (e) { failures++; process.stderr.write(redact(`e2e: stopped: ${e?.code ?? ''} ${String(e?.message ?? e).split('\n')[0]}`) + '\n'); }
|
||||
finally { await close(); }
|
||||
if (failures) process.exitCode = 1;
|
||||
@@ -0,0 +1,120 @@
|
||||
// C2-14 fixture: the real host/launcher with C2-06's scripted citation adapter. No model binary runs.
|
||||
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { BusError, writeGrantEvent } from '../../packages/bus/src/broker.mjs';
|
||||
import { Store } from '../../packages/bus/src/store.mjs';
|
||||
import { startBroker } from '../../packages/bus/src/runtime.mjs';
|
||||
import { grantDecision } from '../../packages/bus/tests/host-fixtures.mjs';
|
||||
import { launchDoc } from '../../packages/business/tests/helpers.mjs';
|
||||
import { fakeHarnesses, fixture } from '../../packages/cli/tests/helpers.mjs';
|
||||
import { bootConfig, loadSystem } from '../../packages/cli/src/config.mjs';
|
||||
import { createLauncher } from '../../packages/cli/src/launcher.mjs';
|
||||
import { SESSION_CREDENTIALS } from '../../packages/seat/src/session.mjs';
|
||||
import * as library from '../../packages/library/src/index.mjs';
|
||||
|
||||
const ADAPTER = fileURLToPath(new URL('../../packages/harness/tests/fixtures/citation-adapter.mjs', import.meta.url));
|
||||
const ROTATE = '2099-01-01';
|
||||
const AT = '2026-10-24T00:00:00.000Z';
|
||||
const shell = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
|
||||
|
||||
// A fixture grant is part of the brief. It uses the normal store shape; no executor/network mint or
|
||||
// role-policy modification occurs. This is deliberately distinct from the row287 grant acceptance.
|
||||
function seedGrant(dataRoot, storeRoot, token) {
|
||||
const dir = join(storeRoot, 'acme', 'bean');
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(dir, 'gitea'), token + '\n', { mode: 0o600 });
|
||||
const store = new Store(dataRoot);
|
||||
try {
|
||||
const decision = grantDecision(store, { business: 'acme', grant: {
|
||||
method: 'gitea', agent: 'bean', target: { projects: ['books'] }, rotateBy: ROTATE,
|
||||
} });
|
||||
store.run('INSERT INTO gitea_users(business,agent,user,id,nonce,decision) VALUES(?,?,?,?,?,?)',
|
||||
'acme', 'bean', 'acme-bean', 1, '2270227022702270', decision);
|
||||
store.run('INSERT INTO gitea_grants(business,agent,user,token_id,token_name,projects,rotate_by,decision) VALUES(?,?,?,?,?,?,?,?)',
|
||||
'acme', 'bean', 'acme-bean', 1, 'fixture-227', '["books"]', ROTATE, decision);
|
||||
writeGrantEvent(store, AT, 'acme', { operation: 'grant.gitea', decision }, decision);
|
||||
writeGrantEvent(store, AT, 'acme', { action: 'grant.applied', method: 'gitea', agent: 'bean',
|
||||
grantDecision: decision, user: 'acme-bean', userId: 1, tokenId: 1, forks: [],
|
||||
repositories: [{ project: 'books', repository: 'fixture', level: 'write' }] }, 'bean');
|
||||
} finally { store.close(); }
|
||||
}
|
||||
|
||||
export async function launchFixture(root, entry, { fetchToken, seatToken, ca }) {
|
||||
const configDir = join(root, 'config'), storeRoot = join(configDir, 'secrets', 'agents');
|
||||
const slotDir = join(configDir, 'secrets', 'acme');
|
||||
mkdirSync(slotDir, { recursive: true, mode: 0o700 });
|
||||
for (const slot of [library.SLOT, library.HUMAN_SLOT])
|
||||
writeFileSync(join(slotDir, slot), fetchToken + '\n', { mode: 0o600 });
|
||||
const plan = join(root, 'citation-plan.json');
|
||||
writeFileSync(plan, '[]\n', { mode: 0o600 });
|
||||
const f = fixture(root, 'acme', doc => {
|
||||
// The unused factory roles remain valid, but no tracker is configured or polled.
|
||||
delete doc.tracker;
|
||||
doc.projects = { books: entry };
|
||||
doc.roles.chief.credentials = { gitea: { file: join(slotDir, library.SLOT), rotateBy: ROTATE } };
|
||||
doc.roles.bean = { definition: 'companion',
|
||||
credentials: { gitea: { store: true, rotateBy: ROTATE } },
|
||||
identity: { name: 'Bean fixture', email: '[email protected]', gitea: { url: new URL(entry.remote).origin, user: 'acme-bean' } },
|
||||
publish: ['books'], vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
|
||||
doc.launch = { ...launchDoc('chief', ['bean'], { opus: 1, sonnet: 1 }), threadIdleSeconds: 3600 };
|
||||
doc.plugins = { ...doc.plugins, channel: ['webui'] };
|
||||
doc.replyLimits = { requireCitation: true, citationsPerReply: 20 };
|
||||
return doc;
|
||||
});
|
||||
chmodSync(configDir, 0o700);
|
||||
for (const key of SESSION_CREDENTIALS.flat()) delete f.env[key];
|
||||
seedGrant(f.dataRoot, storeRoot, seatToken);
|
||||
const system = loadSystem({ env: f.env });
|
||||
const { trackers, ...boot } = bootConfig({ system, businessId: 'acme', env: f.env });
|
||||
if (trackers !== undefined) throw new Error('acceptance fixture must not configure tracker polling');
|
||||
const command = (kind, url, opts) => {
|
||||
const c = library.syncCommand(kind, url, opts);
|
||||
return { argv: c.argv, env: { ...c.env, GIT_SSL_CAINFO: ca } };
|
||||
};
|
||||
const runtime = await startBroker({ ...boot, librarySync: { command, intervalMs: 3_600_000, configDir } });
|
||||
const hooks = [], cleanups = [], caps = new Map(), records = [], logs = [];
|
||||
let closing = null;
|
||||
const close = () => (closing ??= (async () => {
|
||||
try { for (const hook of hooks) await hook(0); }
|
||||
finally {
|
||||
try { await runtime.close(); }
|
||||
finally { for (const cleanup of cleanups.reverse()) await cleanup(); }
|
||||
}
|
||||
})());
|
||||
try {
|
||||
const ops = {
|
||||
identity: m => runtime.identity(m.cap), authorizeLaunch: m => runtime.authorizeLaunch(m.cap, m.instance),
|
||||
refuse: m => runtime.refuse(m.cap, m.code, m.subject), endLaunch: m => runtime.endLaunch(m.record),
|
||||
credentialStatus: m => runtime.credentialStatus(m.business, m.role), threadsState: m => runtime.threadsState(m.business),
|
||||
threadIdle: m => runtime.threadIdle(m.record), mayLaunch: m => runtime.mayLaunch(m),
|
||||
landHeld: m => runtime.landHeld(m.record), landHolders: m => runtime.landHolders(m.business),
|
||||
landRelease: m => runtime.landRelease(m.record), sessionClears: m => runtime.sessionClears(m.business),
|
||||
lifecycleRequests: m => runtime.lifecycleRequests(m.business),
|
||||
};
|
||||
const wrap = fn => async value => {
|
||||
try { return fn(value); }
|
||||
catch (e) { throw Object.assign(new Error('fixture host refused'), { code: e instanceof BusError ? e.code : 'fixture-host-refused' }); }
|
||||
};
|
||||
const host = { business: 'acme', path: runtime.path, dataRoot: f.dataRoot,
|
||||
op: wrap(m => { if (!Object.hasOwn(ops, m.op)) throw new BusError('invalid-host-request'); return ops[m.op](m); }),
|
||||
bindLaunch: wrap(record => {
|
||||
const bound = runtime.bindLaunch(record);
|
||||
records.push(record); caps.set(record.thread, bound.cap);
|
||||
return bound;
|
||||
}),
|
||||
beforeClose: fn => void hooks.push(fn),
|
||||
};
|
||||
const adapter = join(root, 'citation-adapter.sh');
|
||||
writeFileSync(adapter, `cp ${shell(plan)} "$(dirname "$MOSAIC_TURN_MARKER")/fixture-steps.json"\nexec ${shell(process.execPath)} ${shell(ADAPTER)}\n`, { mode: 0o644 });
|
||||
const launcher = createLauncher({ host, system, env: f.env,
|
||||
makeLoader: await fakeHarnesses({ after: fn => cleanups.push(fn) }, { pi: adapter }, write =>
|
||||
write('channel', 'webui', `export * from ${JSON.stringify(new URL('../../packages/plugins/channel/webui/index.mjs', import.meta.url).href)};`)),
|
||||
readOnly: [ADAPTER, plan], harnessVersion: '0.85.1',
|
||||
sessionDefaults: { pollInterval: 50 }, threadTickMs: 50, log: line => logs.push(line),
|
||||
});
|
||||
await launcher.listen();
|
||||
return { ...f, runtime, launcher, logs, records, plan, cap: thread => caps.get(thread), close,
|
||||
business: boot.businesses.acme, setPlan: steps => writeFileSync(plan, JSON.stringify(steps) + '\n') };
|
||||
} catch (e) { await close(); throw e; }
|
||||
}
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# C2-14 (#1728, row227): one acceptance run on a scratch root, no skipped steps.
|
||||
# Entry point for the real launcher/runner, C2-06 scripted adapter, fake TLS Gitea and Console DOM.
|
||||
# S6-S10 wait on row288's landing; do not execute this draft before Sage's CLEAR (sage-4167).
|
||||
# The only human stand-in is bindHuman in-process; a development agent cannot prove human CLI ancestry.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
SANDBOX="$(mktemp -d)"
|
||||
DRIVER_PID=''
|
||||
cleanup() {
|
||||
local result=$?
|
||||
trap - EXIT INT TERM
|
||||
if [ -n "$DRIVER_PID" ]; then
|
||||
kill -TERM "$DRIVER_PID" 2>/dev/null || true
|
||||
wait "$DRIVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$SANDBOX"
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
PASS=0 FAIL=0
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_OK=''; C_FAIL=''; C_RESET=''
|
||||
fi
|
||||
check() {
|
||||
if [ "$2" = 0 ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"
|
||||
else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
||||
}
|
||||
DRIVER=scripts/case2/e2e.mjs
|
||||
STEPS='S1a S1b S2a S2b S2c S2d S3a S3b S3c S4 S5a S5b S6 S7 S8 S9 S10'
|
||||
node --check "$DRIVER" >/dev/null 2>&1
|
||||
check "syntax: $DRIVER" "$?"
|
||||
OUT="$SANDBOX/steps.txt" ERR="$SANDBOX/stderr.txt"
|
||||
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 node "$DRIVER" "$SANDBOX/root" >"$OUT" 2>"$ERR" &
|
||||
DRIVER_PID=$!
|
||||
wait "$DRIVER_PID"
|
||||
RC=$?
|
||||
DRIVER_PID=''
|
||||
check "driver ran to the end (exit $RC)" "$RC"
|
||||
if [ "$RC" -ne 0 ]; then grep '^e2e: ' "$ERR" | head -5; fi
|
||||
seen=''
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
'ok '*) id="${line#ok }"; id="${id%% *}"; check "${line#ok }" 0 ;;
|
||||
'not ok '*) id="${line#not ok }"; id="${id%% *}"; check "${line#not ok }" 1 ;;
|
||||
*) id=''; check 'driver line not understood (a skip cannot pass acceptance)' 1 ;;
|
||||
esac
|
||||
[ -n "$id" ] && seen="$seen $id"
|
||||
done <"$OUT"
|
||||
[ "${seen# }" = "$STEPS" ]
|
||||
check 'every acceptance step ran once, in order, with nothing skipped' "$?"
|
||||
if grep -qF -e 'fx227-fetch-q7' -e 'fx227-seat-q7' "$OUT" "$ERR"; then
|
||||
check 'fixture credentials are in no driver output' 1
|
||||
else
|
||||
check 'fixture credentials are in no driver output' 0
|
||||
fi
|
||||
printf '\nselftest: %s passed, %s failed, 0 skipped\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in new issue
Block a user