Compare commits

...
11 changed files with 732 additions and 0 deletions

No files matched your search

+2
View File
@@ -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.
+98
View File
@@ -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
+114
View File
@@ -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.
+2
View File
@@ -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.
+43
View File
@@ -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');
}
+257
View File
@@ -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;
+120
View File
@@ -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; }
}
+62
View File
@@ -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 ]