fix(cso): give snapshot.json its own 16 MiB cap and actionable capacity errors

/cso refused any repository with more than about 3,100 tracked files before
an audit started: the snapshot manifest was pretty-printed and both its
writer and every reader capped it at the shared 1 MiB private-state limit
(#3068, manifest half of #2993).

- snapshot.json is written compactly and capped by SNAPSHOT_MANIFEST_LIMIT
  (16 MiB), a constant local to the snapshot module.
- readJson takes a per-artifact cap; readSnapshotManifest is the only
  reader of snapshot.json and passes the snapshot cap through recovery and
  the private-file size check. MAX_STATE_FILE and MAX_OUTPUT stay 1 MiB.
- The manifest-cap and 64 MiB source-cap errors name the bound cap, the
  measured value, what counts toward it, the next step and #2993. A single
  file that would cross 64 MiB now hits the same message before it is read.
- Older pretty-printed manifests still load (recheck, inspect).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-08 06:21:59 +00:00
1 parent f67c478f05
commit 25d66fc7ab
4 files changed
+211 -26

No files matched your search

+14 -14
View File
@@ -34,7 +34,7 @@ import {
validateFinding,
validateVerificationRequest,
} from './contracts';
import { capture, containedFile, assertSnapshot } from './snapshot';
import { capture, containedFile, assertSnapshot, readSnapshotManifest } from './snapshot';
import {
assertStateOutside,
event,
@@ -1057,7 +1057,7 @@ function originalBoundary(report: RunReportV3): {
throw new CsoError('INVALID_SCHEMA', 'Recheck evidence requires a linked original finding');
const dir = runDirectory(report.parent.runId),
original = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest,
manifest = readSnapshotManifest(dir),
finding = original.findings.find((item) => item.id === report.parent!.findingId);
if (report.repoId !== original.repoId)
throw new CsoError('INCOMPATIBLE_INPUT', 'Recheck repository identity differs from the original audit');
@@ -1293,7 +1293,7 @@ function submit(args: string[]) {
const input = rawInput as SubmissionV3;
return withLock(dir, () => {
const report = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json'));
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
requireReportingTime(report);
if (report.status !== 'running')
@@ -1399,7 +1399,7 @@ function finish(args: string[]) {
if (args.length) throw new CsoError('INVALID_ARGUMENT', 'finish takes only a run ID');
return withLock(dir, () => {
const report = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json'));
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
for (const c of report.coverage)
if (
@@ -1475,7 +1475,7 @@ async function inspect(args: string[]) {
const { dir } = run(args);
if (args.length) throw new CsoError('INVALID_ARGUMENT', 'inspect takes one run ID');
const report = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
const rawSensitive = readJson(join(dir, 'sensitive-evidence.json')),
sensitiveEvidence = Array.isArray(rawSensitive)
@@ -1505,7 +1505,7 @@ async function inspect(args: string[]) {
async function read(args: string[]) {
const { dir } = run(args);
if (args.length !== 1) throw new CsoError('INVALID_ARGUMENT', 'read requires one path or opaque handle');
const manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
const manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
const selected = resolveSnapshotPath(manifest, args[0], true),
full = containedFile(join(dir, 'readable'), selected.path),
@@ -1514,7 +1514,7 @@ async function read(args: string[]) {
}
async function history(args: string[]) {
const { dir } = run(args),
manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
let selected: string | undefined;
if (args.length) selected = resolveSnapshotPath(manifest, args.shift(), false, 'History path', true).path;
@@ -1537,7 +1537,7 @@ function resume(args: string[]) {
if (args.length) throw new CsoError('INVALID_ARGUMENT', 'resume takes one run ID');
return withLock(dir, () => {
const report = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json'));
manifest = readSnapshotManifest(dir);
if (report.status === 'finished')
throw new CsoError('INVALID_SCHEMA', 'A finished audit cannot be resumed');
assertSnapshot(dir, manifest);
@@ -1671,7 +1671,7 @@ async function scanner(args: string[], sarif = false) {
id,
runId: report.runId,
runDir: dir,
manifest: readJson(join(dir, 'snapshot.json')),
manifest: readSnapshotManifest(dir),
policy: report.policy,
executionDeadline: Date.parse(report.deadline) - 60_000,
platform: platform(),
@@ -1822,7 +1822,7 @@ async function recheck(args: string[], dependencies: CsoCliDependencies) {
throw new CsoError('INVALID_SCHEMA', 'Recheck requires a finished original audit');
// Keep the original immutable while the fresh snapshot is captured and
// until its child lineage report has been durably published.
const oldManifest = readJson(join(originalDir, 'snapshot.json'));
const oldManifest = readSnapshotManifest(originalDir);
const preserveBase = original.policy.diff || Boolean(original.source.baseCommit),
report = await start(
[
@@ -1922,7 +1922,7 @@ function testPlan(args: string[]) {
if (args.length !== 1 || !['node', 'bun', 'python', 'rails'].includes(args[0]))
throw new CsoError('INVALID_ARGUMENT', 'test-plan requires one supported stack');
const stack = args[0] as 'node' | 'bun' | 'python' | 'rails',
manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
const preparation = inspectPreparation(join(dir, 'snapshot'), stack);
if (preparation.status !== 'ready')
@@ -1947,7 +1947,7 @@ function runtimePlan(args: string[]) {
const port = Number(rawPort);
if (!Number.isInteger(port) || port < 1024 || port > 65535)
throw new CsoError('INVALID_ARGUMENT', '--port must be an integer from 1024 to 65535');
const manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
const manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
const preparation = inspectPreparation(join(dir, 'snapshot'), stack);
if (preparation.status !== 'ready')
@@ -2180,7 +2180,7 @@ async function verify(args: string[], dependencies: CsoCliDependencies) {
request = validateVerificationRequest(raw);
return await withLock(dir, async () => {
const report = loadReport(dir),
manifest = readJson(join(dir, 'snapshot.json')) as SnapshotManifest;
manifest = readSnapshotManifest(dir);
assertSnapshot(dir, manifest);
requireTime(report);
if (report.policy.mode !== 'comprehensive' || report.status !== 'running')
@@ -2493,7 +2493,7 @@ async function replay(args: string[], dependencies: CsoCliDependencies) {
throw new CsoError('INCOMPATIBLE_INPUT', 'Supplied source does not match the bundle input hashes');
};
if (fs.existsSync(retained)) {
manifest = readJson(join(stored.dir, 'snapshot.json'));
manifest = readSnapshotManifest(stored.dir);
const expiresAt =
typeof manifest?.expiresAt === 'string' ? Date.parse(manifest.expiresAt) : Number.NaN;
if (!Number.isFinite(expiresAt) || new Date(expiresAt).toISOString() !== manifest.expiresAt)
+35 -7
View File
@@ -15,7 +15,7 @@ import {
MAX_OUTPUT,
} from './contracts';
import { childEnvironment, executable, git, redact, runProcess } from './process';
import { secureDirectory, writeHelperJson, writeJson } from './state';
import { readJson, secureDirectory, writeHelperJson, writeJson } from './state';
import { scan } from '../redact-engine';
import { atomicWriteSync } from '../fs-atomic';
@@ -52,6 +52,21 @@ function omittedPath(path: string): boolean {
const SECRET_FILE =
/(?:^|\/)(?:\.env(?:\..*)?|\.npmrc|\.yarnrc(?:\.yml)?|\.pypirc|pip\.conf|credentials(?:\.yml(?:\.enc)?)?|master\.key|id_(?:rsa|ed25519)|.*\.(?:pem|p12|pfx|key)|AGENTS\.md|CLAUDE\.md|GEMINI\.md|bunfig\.toml)$/i;
const SOURCE_LIMIT = 64 * 1024 * 1024;
/** snapshot.json holds one entry per source file, so it gets its own cap; other private state stays at 1 MiB. */
export const SNAPSHOT_MANIFEST_LIMIT = 16 * 1024 * 1024;
const CAPACITY_ISSUE = 'https://github.com/garrytan/gstack/issues/2993';
const COUNTED_SOURCE =
'every tracked or nonignored untracked file outside dependency and VCS directories (node_modules, .git, .venv, vendor/bundle and similar)';
const mib = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
export function snapshotManifestCapMessage(entries: number, bytes: number): string {
return `Snapshot manifest cap exceeded: ${entries} source entries need a ${mib(bytes)} (${bytes} byte) manifest; the snapshot manifest cap is ${mib(SNAPSHOT_MANIFEST_LIMIT)}. One entry is recorded for ${COUNTED_SOURCE}. Next step: run /cso on a smaller checkout of the code you want audited, and add your entry count to ${CAPACITY_ISSUE}. No supported workaround raises this cap.`;
}
export function sourceCapMessage(bytes: number, files: number): string {
return `64 MiB source cap exceeded: ${files} source files hold about ${mib(bytes)} (${bytes} bytes); the source admission cap is ${mib(SOURCE_LIMIT)}. The full size of ${COUNTED_SOURCE} counts, including files over 1 MiB whose payloads are withheld from the audit. Next step: run /cso on a smaller checkout of the code you want audited, and add your measured size to ${CAPACITY_ISSUE}. No supported workaround raises this cap.`;
}
export function readSnapshotManifest(runDir: string): SnapshotManifest {
return readJson(join(runDir, 'snapshot.json'), SNAPSHOT_MANIFEST_LIMIT) as SnapshotManifest;
}
const SNAPSHOT_ENTRY_LIMIT = 100_000;
const GIT_POINTER_LIMIT = 8192;
export interface SnapshotCaptureLimits {
@@ -611,10 +626,23 @@ export async function capture(
manifest.baseCommit = (await git(repo, ['rev-parse', '--verify', `${base}^{commit}`], home)).trim();
guard();
}
const sourceCapError = () => {
let measured = 0,
files = 0;
for (const candidate of list) {
if (noReadPath(candidate)) continue;
try {
measured += fs.lstatSync(join(repo, candidate)).size;
files++;
} catch {}
}
return new CsoError('MISSING_INPUT', sourceCapMessage(Math.max(measured, total), files));
};
for (const path of list) {
guard();
let size: number;
try {
fs.lstatSync(join(repo, path));
size = fs.lstatSync(join(repo, path)).size;
} catch (error: any) {
if (error?.code === 'ENOENT') {
absentPaths.add(path);
@@ -640,11 +668,11 @@ export async function capture(
guard();
continue;
}
if (total + size > SOURCE_LIMIT) throw sourceCapError();
const { data, mode } = readStable(repo, path, SOURCE_LIMIT);
total += data.length;
guard();
if (total > SOURCE_LIMIT)
throw new CsoError('MISSING_INPUT', 'Source exceeds the 64 MiB snapshot admission limit');
if (total > SOURCE_LIMIT) throw sourceCapError();
const entry: SnapshotEntry = {
path,
pathId: snapshotPathId(repo, path),
@@ -862,11 +890,11 @@ export async function capture(
// The manifest contains helper-computed identities and source pathnames but
// never source payloads. Persist it exactly in private state: generic
// content redaction would silently break the path/hash identity relation.
const serialized = JSON.stringify(manifest, null, 2);
if (Buffer.byteLength(serialized) + 1 > MAX_OUTPUT)
const serialized = JSON.stringify(manifest);
if (Buffer.byteLength(serialized) + 1 > SNAPSHOT_MANIFEST_LIMIT)
throw new CsoError(
'MISSING_INPUT',
'Snapshot manifest exceeds the 1 MiB private-state admission limit',
snapshotManifestCapMessage(entries.length, Buffer.byteLength(serialized) + 1),
);
atomicWriteSync(join(runDir, 'snapshot.json'), serialized + '\n', { mode: 0o600 });
return manifest;
+6 -5
View File
@@ -814,7 +814,7 @@ export function writeJsonExclusive(path: string, value: unknown): void {
throw new CsoError('PERSISTENCE_FAILED', 'Private immutable artifact could not be written');
}
}
function readPrivateJson(path: string): unknown {
function readPrivateJson(path: string, maxBytes: number): unknown {
let fd: number | undefined;
try {
const before = exactLstat(path);
@@ -823,7 +823,7 @@ function readPrivateJson(path: string): unknown {
before.isSymbolicLink() ||
before.nlink !== 1 ||
before.size <= 0 ||
before.size > MAX_STATE_FILE ||
before.size > maxBytes ||
(process.getuid && before.uid !== process.getuid()) ||
(process.platform !== 'win32' && (before.mode & 0o077) !== 0)
)
@@ -857,11 +857,12 @@ function readPrivateJson(path: string): unknown {
} catch {}
}
}
export function readJson(path: string): any {
/** maxBytes is a per-artifact cap; only the snapshot manifest passes a larger one. */
export function readJson(path: string, maxBytes = MAX_STATE_FILE): any {
try {
secureDirectory(dirname(path));
recoverAtomicNoReplaceJson(path, { label: 'Private immutable artifact', maxBytes: MAX_STATE_FILE });
return readPrivateJson(path);
recoverAtomicNoReplaceJson(path, { label: 'Private immutable artifact', maxBytes });
return readPrivateJson(path, maxBytes);
} catch (e) {
if (e instanceof CsoError) throw e;
throw new CsoError('MISSING_INPUT', 'Private state file is missing or invalid');
+156
View File
@@ -0,0 +1,156 @@
// /cso file-count ceiling (#3068, manifest half of #2993): snapshot.json has its own
// 16 MiB cap and compact form, every per-entry list in a 1 MiB private artifact is
// bounded with an omitted count, and both capacity errors say what to do next.
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
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 { MAX_OUTPUT } from '../lib/cso/contracts';
import {
SNAPSHOT_MANIFEST_LIMIT,
capture,
readSnapshotManifest,
snapshotManifestCapMessage,
sourceCapMessage,
} from '../lib/cso/snapshot';
import { newRun, readJson } from '../lib/cso/state';
const ROOT = path.resolve(import.meta.dir, '..');
const launcher = path.join(ROOT, 'bin', process.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher');
const MIB = 1024 * 1024;
const ISSUE = 'https://github.com/garrytan/gstack/issues/2993';
const KEY = ['pass', 'word'].join('');
let root = '';
let state = '';
const originalHome = process.env.GSTACK_HOME;
function git(repo: string, ...args: string[]): string {
const result = spawnSync('/usr/bin/git', ['-C', repo, ...args], { encoding: 'utf8', env: { HOME: root, PATH: '/usr/bin:/bin' }, timeout: 60_000 });
if (result.status) throw new Error(result.stderr);
return result.stdout;
}
function cso(repo: string, args: string[]) {
return spawnSync(launcher, args, { cwd: repo, encoding: 'utf8', env: { HOME: root, GSTACK_HOME: state, PATH: '/usr/bin:/bin' }, timeout: 120_000, maxBuffer: 64 * MIB });
}
function makeRepo(name: string, files: Record<string, string | Buffer>): string {
const repo = path.join(root, name);
fs.mkdirSync(repo);
git(repo, 'init', '-q');
git(repo, 'config', 'user.email', 'fixture@example.test');
git(repo, 'config', 'user.name', 'Fixture');
for (const [file, body] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true });
fs.writeFileSync(path.join(repo, file), body);
}
git(repo, 'add', '-A');
git(repo, 'commit', '-qm', 'fixture');
return repo;
}
function runDir(run: { repoId: string; runId: string }): string {
return path.join(state, 'security', 'cso', run.repoId, run.runId);
}
function writeInput(name: string, value: unknown): string {
const file = path.join(root, name);
fs.writeFileSync(file, JSON.stringify(value));
return file;
}
const application = { actors: ['tenant user'], assets: ['tenant records'], entrypoints: ['GET /users/:id'], tenantBoundaries: ['tenant id'], sensitiveOperations: ['record read'], invariants: ['tenant isolation'] };
const finding = { title: 'Cross-tenant user read', rootCause: 'Tenant query omits caller tenant predicate', location: { path: 'src/users.ts', line: 1, symbol: 'tenantQuery' }, advisoryIds: [], severity: 'high', confidence: 'high', confidenceRationale: 'The caller-to-query trace and missing tenant predicate directly support the finding', evidence: 'supported', attackerControl: 'Authenticated caller chooses the record ID', impact: 'Another tenant record is returned', scenario: 'A tenant supplies a known record ID owned by another tenant and receives that record', trace: ['GET /users/:id', 'tenantQuery', 'findUnique by id'], references: ['src/users.ts:1', 'OWASP API1:2023'], recommendation: 'Bind the lookup predicate to the authenticated tenant identifier', challenge: { reviewer: 'independent-2', independent: true, mode: 'independent_agent', callers: 'Authenticated route forwards the ID', controls: 'Authentication does not bind tenant', counterevidence: 'Opaque IDs reduce guessing but do not authorize', conclusion: 'The authorization invariant is absent' } };
function completeEvidence(report: any, findings: unknown[]) {
return { application, findings, coverage: report.coverage.filter((item: any) => !['snapshot-inputs', 'history-inputs'].includes(item.domain)).map((item: any) => ({ ...item, status: 'assessed', method: 'fresh caller and boundary trace', gaps: [], evidence: ['current captured source'] })), gaps: [] };
}
const USERS = 'export const tenantQuery = (id:string) => db.user.findUnique({where:{id}})\n';
beforeAll(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cso-ceiling-'));
state = path.join(root, 'state');
process.env.GSTACK_HOME = state;
});
afterAll(() => {
if (originalHome === undefined) delete process.env.GSTACK_HOME;
else process.env.GSTACK_HOME = originalHome;
fs.rmSync(root, { recursive: true, force: true });
});
describe('CSO snapshot manifest cap', () => {
test('the snapshot cap is local to snapshot.json; shared limits stay 1 MiB', () => {
expect(MAX_OUTPUT).toBe(MIB);
expect(SNAPSHOT_MANIFEST_LIMIT).toBe(16 * MIB);
const dir = fs.mkdtempSync(path.join(root, 'caps-'));
fs.chmodSync(dir, 0o700);
const big = JSON.stringify({ padding: 'x'.repeat(MIB + 16) });
for (const name of ['report.json', 'snapshot.json']) fs.writeFileSync(path.join(dir, name), big, { mode: 0o600 });
expect(() => readJson(path.join(dir, 'report.json'))).toThrow('Invalid private state file');
expect(() => readJson(path.join(dir, 'snapshot.json'))).toThrow('Invalid private state file');
expect(readSnapshotManifest(dir)).toEqual(JSON.parse(big));
fs.writeFileSync(path.join(dir, 'snapshot.json'), JSON.stringify({ padding: 'x'.repeat(16 * MIB) }), { mode: 0o600 });
expect(() => readSnapshotManifest(dir)).toThrow('Invalid private state file');
});
test('every snapshot.json read in the helper goes through readSnapshotManifest', () => {
const files = fs.readdirSync(path.join(ROOT, 'lib', 'cso')).filter((file) => file.endsWith('.ts'));
for (const file of files) {
const source = fs.readFileSync(path.join(ROOT, 'lib', 'cso', file), 'utf8');
expect(source).not.toMatch(/readJson\(\s*join\([^)]*'snapshot\.json'\)\s*\)/);
}
const cli = fs.readFileSync(path.join(ROOT, 'lib', 'cso', 'cli.ts'), 'utf8');
expect(cli.match(/readSnapshotManifest\(/g)?.length).toBeGreaterThanOrEqual(13);
const snapshot = fs.readFileSync(path.join(ROOT, 'lib', 'cso', 'snapshot.ts'), 'utf8');
expect(snapshot).toContain("readJson(join(runDir, 'snapshot.json'), SNAPSHOT_MANIFEST_LIMIT)");
expect(snapshot).toContain('> SNAPSHOT_MANIFEST_LIMIT');
expect(snapshot).not.toContain('JSON.stringify(manifest, null, 2)');
});
test('both capacity errors name the bound cap, measured value, what counts, next step and #2993', () => {
const manifest = snapshotManifestCapMessage(61_234, 17_000_000);
expect(manifest).toStartWith('Snapshot manifest cap exceeded:');
for (const part of ['61234 source entries', '17000000 byte', 'cap is 16.0 MiB', 'One entry is recorded for every tracked', 'Next step:', ISSUE, 'No supported workaround'])
expect(manifest).toContain(part);
const source = sourceCapMessage(346_199_207, 7_561);
expect(source).toStartWith('64 MiB source cap exceeded:');
for (const part of ['7561 source files', '346199207 bytes', 'cap is 64.0 MiB', 'files over 1 MiB whose payloads are withheld', 'Next step:', ISSUE, 'No supported workaround'])
expect(source).toContain(part);
});
test('the 64 MiB source cap still fails closed, with the actionable message on stderr', () => {
const repo = makeRepo('over-source-cap', { 'src/users.ts': USERS, 'assets/a.bin': Buffer.alloc(40 * MIB, 1), 'assets/b.bin': Buffer.alloc(25 * MIB, 1) });
const result = cso(repo, ['start', '--repo', repo, '--offline']);
expect(result.status).not.toBe(0);
expect(result.stdout).toBe('');
const error = JSON.parse(result.stderr).error;
expect(error.code).toBe('MISSING_INPUT');
expect(error.message).toStartWith('64 MiB source cap exceeded: 3 source files hold about 65.0 MiB');
expect(error.message).toContain(ISSUE);
expect(error.message).toContain('No supported workaround');
}, 120_000);
});
describe('CSO above the old file-count ceiling', () => {
test('a 2,500-file repository still captures and round-trips', async () => {
const files: Record<string, string> = {};
for (let i = 1; i <= 2500; i++) files[`src/pkg/sub/file_number_${i}.ts`] = `export const v${i} = ${i};\n`;
const repo = makeRepo('plain-2500', files),
run = newRun(repo),
manifest = await capture(repo, run.dir);
expect(manifest.entries).toHaveLength(2500);
expect(readSnapshotManifest(run.dir)).toEqual(manifest);
}, 120_000);
test('a retained pretty-printed snapshot.json from an older helper still loads in recheck', () => {
const repo = makeRepo('legacy-pretty', { 'src/users.ts': USERS, 'src/other.ts': 'export const unrelated = true\n' }),
run = JSON.parse(cso(repo, ['start', '--repo', repo, '--scope', 'auth', '--offline']).stdout),
dir = runDir(run),
initial = readJson(path.join(dir, 'report.json'));
expect(cso(repo, ['submit', run.runId, writeInput('legacy-evidence.json', completeEvidence(initial, [finding]))]).status).toBe(0);
expect(cso(repo, ['finish', run.runId]).status).toBe(0);
const file = path.join(dir, 'snapshot.json');
fs.writeFileSync(file, JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2) + '\n');
expect(fs.readFileSync(file, 'utf8')).toContain('\n "entries": [');
expect(cso(repo, ['inspect', run.runId]).status).toBe(0);
const findingId = readJson(path.join(dir, 'report.json')).findings[0].id,
rechecked = cso(repo, ['recheck', findingId, '--run', run.runId, '--repo', repo]);
expect(rechecked.stderr).toBe('');
expect(rechecked.status).toBe(0);
expect(JSON.parse(rechecked.stdout).parent.runId).toBe(run.runId);
}, 120_000);
});