UNTESTED row 227: rebuild scratch acceptance on landed slices

This commit is contained in:
Steelbeak authored and Jason Woltje committed 2026-10-26 17:39:12 -05:00
1 parent 2a728da86c
commit 5a0c801cb8
10 files changed
+567 -247

No files matched your search

+1
View File
@@ -5816,3 +5816,4 @@ Before: agents can read the library; supporting-path and push-credential depende
2026-10-26 | Sage | mos1884/#662 scope after330/331landing | ReadQ222/Q261/Q263 and659/660/662; posted bounded human-only factory settings/triage/defaulttwo-roundmandate/trackedno-exemptionflake scope with10acceptance criteria, task662comment1338 exactwholebody/author13GETverified. No implementation/review/gate launched.660c1335notready(review/merge, noVM); gatefacts1329/1334 retained; target integration/fullrunner confirmation follows readiness, no localfallback. Landedproduct/evidence unchanged;657/658backlog.
2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved.
- 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.
+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
@@ -1879,3 +1879,5 @@ are never rewritten or removed; corrections are new entries.
2026-10-26 | Sage | mos1884/#662 scope after330/331landing | ReadQ222/Q261/Q263 and659/660/662; posted bounded human-only factory settings/triage/defaulttwo-roundmandate/trackedno-exemptionflake scope with10acceptance criteria, task662comment1338 exactwholebody/author13GETverified. No implementation/review/gate launched.660c1335notready(review/merge, noVM); gatefacts1329/1334 retained; target integration/fullrunner confirmation follows readiness, no localfallback. Landedproduct/evidence unchanged;657/658backlog.
2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved.
- 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');
}
+211 -173
View File
@@ -1,219 +1,257 @@
// C2-14 (#1728, row 227): the Case 2 end-to-end steps on a scratch root, driven by
// scripts/test-case2.sh. Usage: node scripts/case2/e2e.mjs <scratch dir>
//
// One process holds her remote (row 199's fake Gitea on 127.0.0.1 over TLS with a scratch CA), a bus
// on a scratch data root and the steps, because the remote's port and CA live only as long as it does.
// It prints one line per step:
// ok <step> <text>
// not ok <step> <text>: <detail>
// skip <step> waits on row <ids>: <text>
// and exits 0 when it got to the end, 1 when a step threw outside its own check.
//
// Two stand-ins, both honest about what they are:
// - The human. human-cli.mjs proves its caller is a human shell and refuses any process with an agent
// among its ancestors (packages/bus/src/human.mjs), so a seat or a reviewer running this script can't
// use it. The driver binds the human in-process with bindHuman, as the bus tests do.
// - The fake harness. No launcher and no model: the driver binds the creative instance's run on her
// thread, claims it, receives her message and answers it.
//
// Nothing here reads a real key or reaches a real host. The token is made up and lives only in the slot.
// 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 { fileURLToPath } from 'node:url';
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 { startBroker } from '../../packages/bus/src/runtime.mjs';
import * as library from '../../packages/library/src/index.mjs';
import { launchFixture } from './launch.mjs';
import { consoleAcceptance } from './console.mjs';
const TOKEN = 'fixture-token-c2-14-made-up-0227';
const USER = 'books-bot';
const BUSINESS = 'acme';
const companion = JSON.parse(readFileSync(fileURLToPath(new URL('../../roles/companion.json', import.meta.url)), 'utf8'));
const lines = [];
const out = (line) => { lines.push(line); process.stdout.write(line + '\n'); };
// step(id, text, fn): fn returns nothing on a pass and throws on a fail.
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) { out(`not ok ${id} ${text}: ${String(e?.code ?? '')} ${String(e?.message ?? e).split('\n')[0].slice(0, 300)}`.trimEnd()); }
catch (e) { failures++; out(`not ok ${id} ${text}: ${e?.code ?? ''} ${String(e?.message ?? e).split('\n')[0].slice(0, 300)}`.trimEnd()); }
}
const skip = (id, rows, text) => out(`skip ${id} waits on row ${rows.join(',')}: ${text}`);
const expect = (cond, message) => { if (!cond) throw new Error(message); };
const refuses = async (promise, code) => {
try { await promise; } catch (e) { if (e?.code === code) return; throw new Error(`refused ${e?.code ?? e?.message}, expected ${code}`); }
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}`);
};
// Every file under `dir` that holds the token, as bytes.
}
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() && readFileSync(file).includes(TOKEN)) return file;
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) {
// Her side: a work repo in her layout's shape, cloned bare into the fake Gitea.
const work = join(root, 'her', 'books');
mkdirSync(work, { recursive: true });
git(work, 'init', '-q', '-b', 'main');
for (const [path, text] of Object.entries(BASE)) {
mkdirSync(dirname(join(work, path)), { recursive: true });
writeFileSync(join(work, path), text);
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');
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: TOKEN });
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);
// Her next commit, pushed to the bare repo; `from` rewinds first, which makes a force-push.
const push = (files, { from = null } = {}) => {
const push = (changed, { from = null } = {}) => {
if (from) git(work, 'reset', '-q', '--hard', from);
for (const [path, text] of Object.entries(files)) {
mkdirSync(dirname(join(work, path)), { recursive: true });
writeFileSync(join(work, path), text);
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, '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');
// Our side: a scratch data root and config dir with the slot, and a bus with one creative instance.
const dataRoot = join(root, 'data'), configDir = join(root, 'config');
mkdirSync(dataRoot, { mode: 0o700 });
mkdirSync(join(configDir, 'secrets', BUSINESS), { recursive: true, mode: 0o700 });
writeFileSync(join(configDir, 'secrets', BUSINESS, library.SLOT), TOKEN + '\n', { mode: 0o600 });
const entry = { ...mappingEntry(), remote: `${gitea.origin}/writer/books.git`, user: USER };
const business = {
id: BUSINESS, human: 'jason', arbiters: { technical: 'bean', delivery: 'bean' },
roles: { bean: { definition: 'companion', authority: companion.authority } },
definitions: { companion: { messaging: companion.messaging } },
projects: { books: entry },
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)); }
}
};
// syncCommand's argv with the scratch CA added, as packages/library/tests/sync.test.mjs does. The
// timer's period is long, so only its pass at start and the steps' own syncs fetch.
const command = (kind, url, opts) => {
const c = library.syncCommand(kind, url, opts);
return { argv: c.argv, env: { ...c.env, GIT_SSL_CAINFO: tls.ca } };
};
const bus = await startBroker({ dataRoot, businesses: { [BUSINESS]: business }, librarySync: { command, intervalMs: 3_600_000, configDir } });
try {
const 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 }));
// The timer's pass at start may still hold the mirror; a step's sync waits it out.
const sync = async (args = {}) => {
for (let i = 0; ; i++) {
try { return await call(human, '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 refusals = () => readRefusals(dataRoot);
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 });
// S1: her layout and the mapping that reads it.
const mapping = library.checkMapping(entry);
await step('S1a', 'her layout classifies: a character is canon, a Playground note is playground, a top-level note is unmapped', () => {
const of = p => library.classify(mapping, p);
expect(of('Book One/Characters/Mara Vell.md').scope === 'canon' && of('Book One/Characters/Mara Vell.md').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 note');
expect(of('Second World/Chapters/01.md').universe === 'second', 'second universe');
});
skip('S1b', [283], 'a supporting folder in the mapping and the editor field');
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');
});
// S2: sync from her bare repo over https, and read at the served commit.
await step('S2a', 'library.sync fetches her bare repo and serves its head', async () => {
const r = await sync();
expect(r.served === remoteHead() && r.served === first, `served ${r.served}, remote ${remoteHead()}`);
expect(r.lastError === null && r.diverged === null, 'state has an error or a divergence');
});
await step('S2b', 'library.file reads her canon and playground notes at the served commit, and refuses an unmapped one', async () => {
const canon = await call(human, 'library.file', { project: 'books', path: 'Book One/Characters/Mara Vell.md' });
expect(canon.commit === first && canon.content.includes(MARKER), 'canon note');
const play = await call(human, 'library.file', { project: 'books', path: 'Book One/Playground/What If.md' });
expect(play.content.includes(MARKER), 'playground note');
await refuses(call(human, 'library.file', { project: 'books', path: 'Inbox Note.md' }), 'project-read-path');
});
await step('S2c', 'her next commit is a fast-forward and the next sync serves it', async () => {
const next = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nSalt and tar. ${MARKER}\n` });
const r = await sync();
expect(r.served === next, `served ${r.served}, expected ${next}`);
});
await step('S2d', 'the token is in the slot only: nowhere under the data root, the mirror included', () => {
const file = holdsToken(dataRoot);
expect(file === null, `token found under the data root (${file === null ? '' : file.slice(dataRoot.length)})`);
});
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');
});
// S3: she asks the creative instance in its thread; the fake harness answers.
let thread, asked;
await step('S3a', 'she makes a thread with the creative instance and asks in it', async () => {
const project = (await call(human, 'project.create', { name: 'books' })).id;
const made = await call(human, 'thread.create', { project, name: 'Book One', instance: 'bean' });
expect(made.instance === 'bean', `instance ${made.instance}`);
thread = made.id;
asked = (await call(human, 'thread.send', { thread, body: 'What colour are Mara Vell\'s eyes?' })).id;
// 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; }
});
await step('S3b', 'the fake harness receives her message on its thread and its reply is stored in her thread', async () => {
const run = broker.bindLaunch({ business: BUSINESS, role: 'bean', run: 'bean-c2-14', harness: 'pi', thread });
await call(run, 'role.claim');
const inbox = await call(run, 'message.receive');
expect(inbox.length === 1 && inbox[0].id === asked, `received ${inbox.length}`);
await call(run, 'message.send', { to: 'human', body: 'Grey. (Book One/Characters/Mara Vell.md)', in_reply_to: asked });
const history = await call(human, 'thread.messages', { thread });
expect(history.length === 2 && history[1].from_role === 'bean' && history[1].in_reply_to === asked,
`history ${history.map(m => m.from_role).join(',')}`);
});
skip('S3c', [216, 217], 'the reply carries cites from the lookups, the guard verifies each one at the served commit, and the store keeps them');
skip('S4', [217], 'a reply citing a playground note with "as": "canon" refuses playground-cited-as-canon');
// S5: a force-push holds as library-diverged until she accepts the new tip.
await step('S5a', 'a force-push of her branch holds as library-diverged and served stays put', async () => {
const before = (await call(human, 'library.status', { project: 'books' })).served;
const forced = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nRewritten. ${MARKER}\n` }, { from: first });
await refuses(sync(), 'library-diverged');
const status = await call(human, 'library.status', { project: 'books' });
expect(status.served === before, 'served moved');
expect(status.diverged?.commit === forced, `diverged ${JSON.stringify(status.diverged)}`);
expect(refusals().includes('library-diverged'), 'no recorded refusal');
});
await step('S5b', 'accept refuses a stale commit, then serves the new tip and clears the hold', async () => {
await refuses(sync({ accept: first }), 'library-accept-stale');
const tip = remoteHead();
const r = await sync({ accept: tip });
expect(r.served === tip && r.diverged === null, `served ${r.served}`);
});
// S6-S9: agent writes, the story-path refusal, her revert and her Console save.
skip('S6', [283, 285, 286, 288], 'a seat with a fixture grant writes a supporting doc: one commit with the trailer in the bare repo, and the notice in her thread');
skip('S7', [283, 288], 'a write to a story path refuses library-write-story-path, and the bare repo\'s ref is unchanged');
skip('S8', [288], 'her revert of the write lands as one commit');
skip('S9', [283, 288], 'her library.file.save lands as one commit with her editor as author');
// S10: the Console.
skip('S10', [220, 225], 'the Console renders the reply and the notice');
} finally {
await bus.close();
await gitea.close();
}
}
// The codes of the bus's recorded refusals, oldest first, read from its store on a read-only handle.
function readRefusals(dataRoot) {
const db = new DatabaseSync(join(dataRoot, 'bus', 'bus.sqlite'), { readOnly: true });
try { return db.prepare("SELECT body FROM events WHERE kind='action.refused' ORDER BY seq").all().map(row => JSON.parse(row.body).code); }
finally { db.close(); }
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) { process.stderr.write(`e2e: stopped: ${e?.code ?? ''} ${String(e?.message ?? e).split('\n')[0]}\n`); process.exitCode = 1; }
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; }
}
+42 -74
View File
@@ -1,94 +1,62 @@
#!/usr/bin/env bash
# Case 2 end-to-end acceptance on a scratch root (C2-14, #1728, row 227).
#
# Her layout in a bare repo behind a fake Gitea on 127.0.0.1 (TLS, scratch CA),
# a bus on a scratch data root with one creative instance, and the steps of the
# C2-14 brief in order. scripts/case2/e2e.mjs runs the steps in one process,
# since the remote lives only as long as that process does, and prints one line
# per step. This script checks its syntax, runs it, and counts the lines.
#
# No live host, no real model, no network past 127.0.0.1, no real key. The
# human is an in-process stand-in: human-cli.mjs refuses a caller with an agent
# among its ancestors, so a seat can't drive it. The creative instance's run is
# a fake harness bound by the driver, with no launcher.
#
# A step that waits on a row not yet on refactor prints a skip line naming the
# row, and counts as neither pass nor fail. When the row lands, its step gets
# written in the driver and its skip line goes.
#
# scripts/test-case2.sh full run
# NO_COLOR=1 scripts/test-case2.sh plain output
# 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)"
trap 'rm -rf "$SANDBOX"' EXIT
PASS=0
FAIL=0
SKIP=0
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=""
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
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"
# The steps the driver must print, in order. A missing or extra line fails.
STEPS="S1a S1b S2a S2b S2c S2d S3a S3b S3c S4 S5a S5b S6 S7 S8 S9 S10"
echo "toolchain: node $(node --version), git $(git --version | cut -d' ' -f3), openssl $(openssl version | cut -d' ' -f2)"
echo
# --- syntax ---
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" $?
# --- the steps ---
# The driver's own state stays in the sandbox. Git reads no global or system
# config, so a caller's hooks, signing or credential helpers can't reach it.
OUT="$SANDBOX/steps.txt"
ERR="$SANDBOX/stderr.txt"
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
node "$DRIVER" "$SANDBOX/root" >"$OUT" 2>"$ERR"
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
# The driver prints codes and first lines only; never git's stderr with a token in it.
grep '^e2e: ' "$ERR" | head -5
fi
seen=""
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 ;;
"skip "*)
id="${line#skip }"; id="${id%% *}"
SKIP=$((SKIP+1)); echo "skip ${line#skip }" ;;
*)
id=""
check "driver line not understood: ${line:0:200}" 1 ;;
'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 "the driver printed each step once, in order" $?
# --- the token ---
# The made-up token is in the slot and nowhere else the bus writes. The driver
# checks the data root; this checks its output too.
! grep -rqF 'fixture-token-c2-14-made-up-0227' "$OUT" "$ERR"
check "the fixture token is in no driver output" $?
echo
echo "selftest: $PASS passed, $FAIL failed, $SKIP skipped (each skip names the row it waits on)"
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 ]