Files
gstack/test/helpers/docsync-fault-actor.ts
Garry Tanandcapy-ai[bot] a59b5e4097 v1.91.179.0 test(docsync): cut per-case spawn cost in the fault interface and split its outer run (flaky pass 2026-10-22) (#3342)
* test(docsync): cut per-case spawn cost in the fault interface and split its outer run (GSTA-63)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: docsync-fault-interface per-case cost (GSTA-63)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.179.0 stamp

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-22 20:10:23 +00:00

373 lines
23 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import type { fixtureDocs } from './docsync-fixture';
import { DOC_PATH, docsSnapshotArgv, gitAt, gitBlobId, repoContents, repoInventory, type DocsCandidateRecord } from './docsync-repo';
import { extractDocsDispatch } from './docsync-contract';
import { docsNativeInterface } from './docsync-interface';
export const DOCS_CHECKPOINT_MARKER = '<!-- DOCSYNC_CHECKPOINT -->';
export type DocsFault = 'missing-marker' | 'missing-asset' | 'launch-failure' | 'timeout-unsettled' |
'late-result' | 'stale-before' | 'stale-after' | 'recovery' | 'legacy-completion';
export interface ActorEvent { action: string; audit_id?: string; task_id?: string; detail?: string; }
export interface DocsActorState {
root: string;
scenario: DocsFault;
events: ActorEvent[];
tasks: Array<{ id: string | null; audit_id: string; settled: boolean; stopRequested: boolean; elapsed_ms: number;
prompt: string; candidate: string; prompt_sha256: string; candidate_sha256: string;
observed_candidate: DocsCandidateRecord }>;
repaired: boolean;
armed: boolean;
lateChanged: boolean;
acceptedId: string | null;
}
export function docsActorCanRepair(scenario: DocsFault): boolean {
return scenario === 'recovery' || scenario === 'late-result';
}
function owned(root: string, file: string): string {
const absolute = path.resolve(file);
if (!absolute.startsWith(fs.realpathSync(root) + path.sep)) throw Error('actor path outside fixture');
const existing = fs.existsSync(absolute) ? absolute : path.dirname(absolute);
if (fs.realpathSync(existing) !== fs.realpathSync(root) && !fs.realpathSync(existing).startsWith(fs.realpathSync(root) + path.sep)) throw Error('actor symlink escape');
return absolute;
}
function load(file: string): DocsActorState {
const s = JSON.parse(fs.readFileSync(file, 'utf8')) as DocsActorState;
owned(s.root, file);
return s;
}
function save(file: string, state: DocsActorState) {
fs.writeFileSync(file, JSON.stringify(state), { mode: 0o600 });
}
function locked<T>(file: string, body: () => T): T {
const lock = owned(load(file).root, file + '.lock');
const fd = fs.openSync(lock, 'wx', 0o600);
try { return body(); }
finally { fs.closeSync(fd); fs.unlinkSync(lock); }
}
const SNAPSHOT_TIMEOUT_MS = 30_000;
const shellWord = (arg: string) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
export const SNAPSHOT_STDERR_LIMIT = 800;
export type SnapshotFailureReason = 'snapshot_timeout' | 'snapshot_spawn_error' | 'snapshot_exit' | 'snapshot_invalid_record';
class SnapshotFailure extends Error {
constructor(readonly reason: SnapshotFailureReason, readonly status: string, readonly stderr: string) { super(reason); }
}
/**
* The tail of the helper's stderr with secrets redacted and the fixture root replaced by <HOME>, at most SNAPSHOT_STDERR_LIMIT characters.
* The redaction engine loads only here, on a failed snapshot, so hook and CLI spawns skip its import.
*/
function stderrExcerpt(root: string, stderr: string): string {
const { redactFindingSpans } = require('../../lib/redact-engine') as typeof import('../../lib/redact-engine');
const redacted = redactFindingSpans(stderr.replaceAll(root, '<HOME>').trim());
if (redacted === null) return '[stderr omitted: unredactable secret]';
return redacted.length > SNAPSHOT_STDERR_LIMIT ? '…' + redacted.slice(-(SNAPSHOT_STDERR_LIMIT - 1)) : redacted;
}
/**
* Runs the installed `gstack-docs-candidate snapshot` (the section's Prepare helper) for the fixture selection
* and returns the record it wrote, or throws a SnapshotFailure with a stable reason. `out` must not exist yet.
*/
function installedSnapshot(s: DocsActorState, auditId: string, out: string): { record: DocsCandidateRecord; command: string } {
const repo = path.join(s.root, 'repo');
const argv = docsSnapshotArgv({ repo, skills: path.join(s.root, '.claude/skills/gstack') }, auditId, out);
const run = spawnSync(argv[0]!, argv.slice(1), { cwd: repo, encoding: 'utf8', timeout: SNAPSHOT_TIMEOUT_MS,
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } });
const stderr = run.stderr || '';
if (run.error) {
const timedOut = (run.error as NodeJS.ErrnoException).code === 'ETIMEDOUT';
throw new SnapshotFailure(timedOut ? 'snapshot_timeout' : 'snapshot_spawn_error', run.error.message, stderr);
}
if (run.status !== 0) throw new SnapshotFailure('snapshot_exit', `exit ${run.status ?? run.signal}`, stderr);
let record: DocsCandidateRecord;
try { record = JSON.parse(fs.readFileSync(out, 'utf8')) as DocsCandidateRecord; }
catch (error) { throw new SnapshotFailure('snapshot_invalid_record', `exit 0, unreadable record: ${(error as Error).message}`, stderr); }
if (record.schema_version !== 1 || record.audit_id !== auditId || record.mode !== 'edit' || record.base !== argv[argv.indexOf('--base') + 1]
|| !Array.isArray(record.index) || !record.hashes || typeof record.hashes !== 'object') throw new SnapshotFailure('snapshot_invalid_record', 'exit 0, invalid record', stderr);
return { record, command: argv.map(shellWord).join(' ') };
}
/** The candidate prepare would save now, observed without touching any saved artifact. */
function observeCandidate(s: DocsActorState, auditId: string): DocsCandidateRecord {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-actor-observe-'));
try { return installedSnapshot(s, auditId, path.join(dir, 'observed.json')).record; }
finally { fs.rmSync(dir, { recursive: true, force: true }); }
}
function changeCandidate(s: DocsActorState) {
fs.writeFileSync(path.join(s.root, 'repo/app.ts'), 'export const format = "json";\n');
s.lateChanged = true;
s.armed = false;
s.events.push({ action: 'scheduled-input-edit', detail: 'app.ts' });
}
export function docsActorCommand(file: string, action: string, args: Record<string, string> = {}): { text: string; exit: number } {
return locked(file, () => {
const s = load(file);
let text = '';
let exit = 0;
const last = () => {
const task = s.tasks.find(t => t.id === args.task_id);
if (!task || !args.task_id) throw Error('unknown fixture child');
return task;
};
const complete = (auditId: string, status: 'current' | 'updated' | 'blocked', blockers: string[] = [], updated: string[] = []) => {
return `SESSION_KIND: ${blockers.includes('Missing spawned marker') ? 'interactive' : 'spawned'}\n` + JSON.stringify({
schema_version: 1, audit_id: auditId, status, files_updated: updated,
files_reviewed: blockers.length ? [] : [DOC_PATH],
documentation_section: `${status} — fixture child audit ${auditId}; ${blockers.length ? blockers.join('; ') : 'reviewed the selected command reference'}.`,
blockers, decisions: [],
});
};
try {
if (action === 'prepare') {
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(args.audit_id ?? '')) throw Error('prepare requires a fresh literal audit id');
if (s.tasks.some(t => !t.settled)) throw Error('attempted snapshot with unsettled writer');
const failedPrepares = s.events.filter(e => e.action === 'prepare-failed');
if (failedPrepares.some(e => e.audit_id === args.audit_id)) throw Error(`reason=snapshot_retry_needs_fresh_id: prepare ${args.audit_id} failed; retry once with a fresh audit_id`);
if (failedPrepares.length >= 2) throw Error('reason=snapshot_retry_exhausted: prepare already failed and was retried once; report the documentation phase blocked');
const repo = path.join(s.root, 'repo');
const skills = path.join(s.root, '.claude/skills/gstack');
const candidateFile = owned(s.root, path.join(s.root, `candidate-${args.audit_id}.json`));
const promptFile = owned(s.root, path.join(s.root, `prompt-${args.audit_id}.md`));
const saved = (file: string) => fs.lstatSync(file, { throwIfNoEntry: false }) !== undefined;
if (saved(candidateFile) || saved(promptFile)) throw Error('prepare cannot overwrite a saved snapshot or prompt');
const source = extractDocsDispatch(fs.readFileSync(path.join(skills, 'ship/sections/documentation.md'), 'utf8'));
let snapshot: ReturnType<typeof installedSnapshot>;
try { snapshot = installedSnapshot(s, args.audit_id, candidateFile); }
catch (error) {
if (!(error instanceof SnapshotFailure)) throw error;
const failureFile = owned(s.root, path.join(s.root, `prepare-${args.audit_id}-failure.json`));
const kept = [...(saved(candidateFile) ? [candidateFile] : []), failureFile];
const failure = { reason: error.reason, audit_id: args.audit_id, status: error.status,
stderr_excerpt: stderrExcerpt(s.root, error.stderr), kept,
retry: failedPrepares.length ? 'none left' : 'at most once, with a fresh audit_id' };
fs.writeFileSync(failureFile, JSON.stringify(failure, null, 2), { flag: 'wx', mode: 0o600 });
s.events.push({ action: 'prepare-failed', audit_id: args.audit_id, detail: JSON.stringify(failure) });
save(file, s);
return { text: `prepare failed: reason=${failure.reason} (${failure.status}); retry ${failure.retry}; kept for inspection: ${kept.join(', ')}\nstderr (redacted, last ${SNAPSHOT_STDERR_LIMIT} chars):\n${failure.stderr_excerpt}`, exit: 25 };
}
const candidate = snapshot.record;
const prompt = source.replaceAll('${HOME}', s.root).replaceAll('<branch>', candidate.branch)
.replaceAll('<base>', 'main').replaceAll('<candidate-path>', candidateFile)
.replaceAll('<audit-id>', args.audit_id).replaceAll('<mode>', candidate.mode)
+ '\n\n' + docsNativeInterface({ home: s.root, repo, skills });
fs.writeFileSync(promptFile, prompt, { flag: 'wx', mode: 0o600 });
s.events.push({ action, audit_id: args.audit_id, detail: JSON.stringify({ candidate, prompt, snapshot_command: snapshot.command }) });
text = JSON.stringify({ audit_id: args.audit_id, candidate: candidateFile, prompt: promptFile, snapshot_command: snapshot.command });
} else if (action === 'dispatch') {
if (!args.audit_id || args.run_in_background !== 'false') throw Error('dispatch requires identity and explicit foreground flag');
if (s.tasks.some(t => !t.settled)) throw Error('attempted writer overlap');
if (s.events.some(e => e.action === 'dispatch' && e.audit_id === args.audit_id)) throw Error('reused audit identity');
if (s.events.some(e => e.action === 'prepare-failed' && e.audit_id === args.audit_id)) throw Error('dispatch of a failed prepare');
const prompt = fs.readFileSync(owned(s.root, args.prompt), 'utf8');
const candidate = fs.readFileSync(owned(s.root, args.candidate));
if (!prompt.includes('document-release') || !prompt.includes('files_updated') || !prompt.includes(args.audit_id)) throw Error('dispatch did not carry the actual workflow prompt');
const task = { id: s.scenario === 'launch-failure' ? null : `fixture-child-${s.tasks.length + 1}`, audit_id: args.audit_id, settled: true, stopRequested: false, elapsed_ms: 0,
prompt, candidate: candidate.toString('utf8'), prompt_sha256: new Bun.CryptoHasher('sha256').update(prompt).digest('hex'),
candidate_sha256: new Bun.CryptoHasher('sha256').update(candidate).digest('hex'),
observed_candidate: observeCandidate(s, args.audit_id) };
s.events.push({ action, audit_id: args.audit_id, ...(task.id === null ? {} : { task_id: task.id }) });
if (s.scenario === 'missing-asset') throw Error('missing installed asset must block before dispatch');
s.tasks.push(task);
if (s.scenario === 'launch-failure') {
exit = 23;
text = 'Child launch failed: injected unavailable worker. No child was started.';
save(file, s);
return { text, exit };
}
if (s.scenario === 'legacy-completion') {
const doc = owned(s.root, path.join(s.root, 'repo', DOC_PATH));
fs.writeFileSync(doc, fs.readFileSync(doc, 'utf8').replace('Default format: text.', 'Default format: JSON.'));
s.events.push({ action: 'partial-doc-edit', audit_id: args.audit_id, detail: DOC_PATH });
text = 'SESSION_KIND: spawned\n' + JSON.stringify({ files_updated: [], commit_sha: null, pushed: false, documentation_section: null });
} else if (s.scenario === 'missing-marker' || s.scenario === 'recovery' && !s.repaired) {
text = complete(args.audit_id, 'blocked', ['Missing spawned marker']);
} else if (s.scenario === 'timeout-unsettled' || s.scenario === 'late-result' && s.tasks.length === 1) {
task.settled = false;
text = JSON.stringify({ task_id: task.id, status: 'running', elapsed_ms: 0, virtual_clock: true });
} else if (s.scenario === 'late-result') {
if (!s.repaired) throw Error('transport must be repaired before retry');
text = complete(s.tasks[0].audit_id, 'current');
s.events.push({ action: 'late-callback', audit_id: s.tasks[0].audit_id });
} else if (s.scenario.startsWith('stale-') && s.tasks.length === 1) {
if (s.scenario === 'stale-before') changeCandidate(s);
else s.armed = true;
text = complete(args.audit_id, 'current');
s.events.push({ action: 'completion', audit_id: args.audit_id });
} else {
const updated: string[] = [];
if (s.lateChanged) {
const doc = path.join(s.root, 'repo', DOC_PATH);
fs.writeFileSync(doc, fs.readFileSync(doc, 'utf8').replace('Default format: text.', 'Default format: JSON.'));
updated.push(DOC_PATH);
s.events.push({ action: 'factual-doc-edit', audit_id: args.audit_id, detail: DOC_PATH });
}
s.acceptedId = args.audit_id;
text = complete(args.audit_id, updated.length ? 'updated' : 'current', [], updated);
s.events.push({ action: 'completion', audit_id: args.audit_id });
}
} else if (action === 'inspect') {
if (Object.keys(args).length) throw Error('inspect takes no arguments');
if (s.armed) changeCandidate(s);
const repo = path.join(s.root, 'repo');
const inventory = repoInventory(repo);
if (inventory.length > 64) throw Error('inspect inventory exceeds the bound');
const contents = repoContents(repo, inventory);
const [head, base] = gitAt(repo, 'rev-parse', 'HEAD', 'main').split('\n');
const files = Object.fromEntries(inventory.map(rel => {
const bytes = contents[rel];
if (bytes === undefined) return [rel, { exists: false }];
const buffer = Buffer.from(bytes, 'base64');
return [rel, { exists: true, hash: gitBlobId(buffer), content: buffer.toString('utf8') }];
}));
text = JSON.stringify({
operation: 'inspect', base, head,
branch: gitAt(repo, 'branch', '--show-current'), index: gitAt(repo, 'ls-files', '-s', '-z').split('\0').filter(Boolean),
pre_existing_dirty: gitAt(repo, 'status', '--porcelain', '-z'),
diff_committed: gitAt(repo, 'diff', base, 'HEAD'),
diff_cached: gitAt(repo, 'diff', '--cached'), diff_worktree: gitAt(repo, 'diff'),
inventory, files,
});
s.events.push({ action, detail: new Bun.CryptoHasher('sha256').update(text).digest('hex') });
} else if (action === 'status') {
const task = last();
task.elapsed_ms += task.stopRequested ? 300_001 : 600_001;
s.events.push({ action, task_id: args.task_id, detail: task.settled ? 'settled' : 'running' });
text = JSON.stringify({ task_id: task.id, status: task.settled ? 'stopped' : 'running', settled: task.settled,
elapsed_ms: task.elapsed_ms, virtual_clock: true });
} else if (action === 'stop') {
const task = last();
task.stopRequested = true;
if (s.scenario !== 'timeout-unsettled') task.settled = true;
s.events.push({ action, task_id: args.task_id, detail: task.settled ? 'settled' : 'unsettled' });
text = JSON.stringify({ task_id: task.id, stop_requested: true, settled: task.settled });
} else if (action === 'repair') {
if (!docsActorCanRepair(s.scenario) || s.repaired || !s.tasks.length || s.tasks.some(t => !t.settled)) throw Error('repair not available');
s.repaired = true;
s.events.push({ action, detail: 'fixture transport/marking repaired' });
text = 'Fixture transport/marking repaired; future dispatches use the corrected launcher.';
} else if (action === 'publish') {
s.events.push({ action, audit_id: args.audit_id });
if (!s.acceptedId || args.audit_id !== s.acceptedId || s.tasks.some(t => !t.settled)) throw Error('publication attempted without a current settled audit');
const report = fs.readFileSync(owned(s.root, args.report), 'utf8');
if (!report.includes(s.acceptedId)) throw Error('publication did not consume actual audit result');
fs.writeFileSync(path.join(s.root, 'publication.json'), JSON.stringify({ audit_id: s.acceptedId, report }), { mode: 0o600 });
text = 'Mock publication recorded.';
} else throw Error('unsupported fixture action');
} catch (error) {
s.events.push({ action: 'rejected', audit_id: args.audit_id, detail: String(error) });
text = String(error);
exit = 24;
}
save(file, s);
return { text, exit };
});
}
export function docsActorHook(file: string, input: string) {
return locked(file, () => {
const s = load(file);
const e = JSON.parse(input);
if (s.armed && e.hook_event_name === 'PreToolUse' && e.cwd === path.join(s.root, 'repo') &&
!JSON.stringify(e.tool_input ?? {}).includes('docsync-fault-actor.ts')) {
changeCandidate(s);
save(file, s);
}
});
}
export function installDocsActor(fixture: ReturnType<typeof fixtureDocs>, scenario: DocsFault): string {
fs.writeFileSync(fixture.invocation, `# Bounded ship fixture invocation
This is synthetic prior-stage state supplied by the test, not evidence that real reviews or checks ran. Steps 0–14 are complete only within this isolated documentation-phase fixture. Do not reconstruct or rerun them.
## Release
Base main (${gitAt(fixture.repo, 'rev-parse', 'main')}); HEAD ${fixture.before.head}; VERSION 0.1.0.0; package version 0.1.0; BUMP_LEVEL not applicable to this bounded phase. Existing metadata is intentional fixture input, not a request to repair release preparation.
## Decisions
No risk exceptions or risky edits approved. Preserve unrelated and partial content. No real publication or later ship steps authorized.
## Reviews
Earlier review stages are synthetic and outside this fixture. No live review handles or tokens are asserted.
## Checks
Earlier check stages are synthetic and outside this fixture. No test receipts are asserted.
## Initial documentation state
Attempts used: 0. No accepted audit, hashes, exception or child handle. The supplied candidate.json is initial fixture input, not an accepted audit; prepare saves the same selection with current hashes, so it needs no separate Read.
## Initial next steps
1. CURRENT: documentation phase (Step 14.5, or store documentation preflight).
2. Save the result and optionally execute the authorized local publication stand-in if the actual documentation gate permits it.
3. STOP before Step 15 or any store action.
## Documentation checkpoint journal
Append changes in order. The latest stated value is current; earlier entries and the initial state remain evidence, not instructions to repeat completed work.
${DOCS_CHECKPOINT_MARKER}
`, { mode: 0o600 });
const file = path.join(fixture.home, 'actor-state.json');
save(file, { root: fixture.home, scenario, events: [], tasks: [], repaired: false, armed: false, lateChanged: false, acceptedId: null });
const configFile = path.join(fixture.env.CLAUDE_CONFIG_DIR, 'settings.json');
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
const quote = (p: string) => `'${p.replaceAll("'", "'\\''")}'`;
config.hooks.PreToolUse.push({ matcher: '^(Bash|Read|Write|Edit|Glob|Grep)$', hooks: [{ type: 'command',
command: `${quote(process.execPath)} ${quote(import.meta.path)} hook ${quote(file)}`, timeout: 5 }] });
fs.writeFileSync(configFile, JSON.stringify(config));
if (scenario === 'missing-asset') fs.unlinkSync(path.join(fixture.skills, 'document-release/sections/audit-scope.md'));
return file;
}
export const DOCS_SEEDED_AUDIT_ID = 'ship-docs-a1';
const DOCS_CHILD_ASSETS = ['document-release/SKILL.md', 'document-release/sections/audit-scope.md', 'document-release/sections/release-body.md'];
export function docsActorSeeded(scenario: DocsFault): boolean {
return scenario !== 'missing-asset' && scenario !== 'legacy-completion';
}
export function seedDocsFirstAttempt(fixture: ReturnType<typeof fixtureDocs>, file: string) {
const assets = DOCS_CHILD_ASSETS.map(relative => {
const asset = path.join(fixture.skills, relative);
if (!fs.existsSync(asset)) throw Error(`seeded attempt requires installed ${relative}`);
return { asset, sha256: new Bun.CryptoHasher('sha256').update(fs.readFileSync(asset)).digest('hex') };
});
const prepared = docsActorCommand(file, 'prepare', { audit_id: DOCS_SEEDED_AUDIT_ID });
if (prepared.exit) throw Error(`seeded prepare failed: ${prepared.text}`);
const { candidate, prompt, snapshot_command } = JSON.parse(prepared.text) as { candidate: string; prompt: string; snapshot_command: string };
const dispatched = docsActorCommand(file, 'dispatch', { audit_id: DOCS_SEEDED_AUDIT_ID, candidate, prompt, run_in_background: 'false' });
if (dispatched.exit === 24) throw Error(`seeded dispatch rejected: ${dispatched.text}`);
const completion = path.join(fixture.home, `completion-${DOCS_SEEDED_AUDIT_ID}.md`);
fs.writeFileSync(completion, dispatched.text, { flag: 'wx', mode: 0o600 });
let handle = '';
try { handle = JSON.parse(dispatched.text).task_id ?? ''; } catch {}
const record = fs.readFileSync(fixture.invocation, 'utf8');
if (record.split(DOCS_CHECKPOINT_MARKER).length !== 2) throw Error('invocation journal marker must occur exactly once');
fs.writeFileSync(fixture.invocation, record.replace(DOCS_CHECKPOINT_MARKER, () => `### Checkpoint 1 — attempt 1 (fixture-owned prior state)
Attempts used: 1. Prepare step 1: installed child assets present with sha256 ${assets.map(a => `${a.asset} ${a.sha256}`).join('; ')}. Prepare steps 2–3: audit ${DOCS_SEEDED_AUDIT_ID}; candidate ${candidate}, written by ${snapshot_command}; prompt ${prompt}. Launched with dispatch run_in_background=false; dispatch exit code ${dispatched.exit}; its verbatim output is saved once at ${completion}.${handle ? ` Returned child handle: ${handle}.` : ''}
Next: Parent processing for attempt 1, starting at Collect. A repeated Prepare may confirm step 1 with one literal sha256sum of these three asset paths instead of re-reading them; a missing asset or changed hash blocks before launch.
${DOCS_CHECKPOINT_MARKER}`));
return { auditId: DOCS_SEEDED_AUDIT_ID, candidate, prompt, snapshotCommand: snapshot_command, completion, exit: dispatched.exit, text: dispatched.text,
events: load(file).events.length };
}
if (import.meta.main) {
const [action, file, ...rest] = process.argv.slice(2);
if (action === 'hook') docsActorHook(file, fs.readFileSync(0, 'utf8'));
else {
const args = Object.fromEntries(rest.map(arg => {
const at = arg.indexOf('=');
if (at < 1) throw Error('fixture arguments use key=value');
return [arg.slice(0, at), arg.slice(at + 1)];
}));
const result = docsActorCommand(file, action, args);
console.log(result.text);
process.exitCode = result.exit;
}
}