Merge remote-tracking branch 'origin/main' into gsta258-s1-envrc-redact

This commit is contained in:
garrytan committed 2026-11-03 04:22:21 +00:00
commit 735ddb2654
39 files changed
+1516 -71

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
/**
* lib/ci-carry.ts (GSTA-258 piece 1; design fixes A2, A3): CI's verified run at HEAD's exact, clean tree stands in
* for running a lane again. The dependencies are fakes, so nothing here reaches GitHub.
*/
import { describe, expect, test } from 'bun:test';
import { findCarry, shipLanesErrors, waitForRun } from '../lib/ci-carry';
import { validateConfig } from '../lib/pregate/config';
import { receiptSummaryLine, validateReceipt } from '../lib/ship-receipt';
import type { ApiJob, ApiRun, RunDeps, RunProfile, RunSource, TreeRecord } from '../lib/run-evidence';
const REPO = 'garrytan/gstack';
const HEAD = 'a'.repeat(40);
const TREE = 'b'.repeat(40);
const NOW = new Date('2026-11-02T12:00:00Z');
const PROFILE: RunProfile = { workflow: 'free-tests.yml', events: ['pull_request', 'workflow_dispatch'], jobs: ['free-tests'], artifact: 'tested-tree', os: 'Linux' };
const run = (id: number, over: Partial<ApiRun> = {}): ApiRun => ({
id, run_attempt: 1, status: 'completed', conclusion: 'success', event: 'workflow_dispatch', head_sha: HEAD, path: '.github/workflows/free-tests.yml',
created_at: `2026-11-02T1${id % 10}:00:00Z`.replace('T1', 'T0'), updated_at: '2026-11-02T11:30:00Z', repository: { full_name: REPO }, head_repository: { full_name: REPO }, ...over,
});
const record = (id: number, over: Partial<TreeRecord> = {}): TreeRecord => ({ run_id: id, run_attempt: 1, event: 'workflow_dispatch', sha: HEAD, tree: TREE, files: ['test/a.test.ts', 'test/b.test.ts'], tests: 40, os: 'Linux', ...over });
function source(runs: ApiRun[], records: Record<number, TreeRecord> = {}, jobs: ApiJob[] = [{ name: 'free-tests', conclusion: 'success', completed_at: '2026-11-02T11:30:00Z' }]): RunSource {
const deps: RunDeps = {
run: id => { const r = runs.find(x => x.id === id); if (!r) throw new Error('HTTP 404'); return r; },
runsForHead: (_wf, sha) => runs.filter(r => r.head_sha === sha),
jobs: () => jobs,
artifact: id => { if (!records[id]) throw new Error('no artifact'); return records[id]!; },
commitTree: sha => (sha === HEAD ? TREE : null),
};
return { repo: REPO, deps, profiles: { 'ci-reuse/free-tests': PROFILE }, now: NOW };
}
const clean = () => TREE;
const carry = (src: RunSource, over: Partial<Parameters<typeof findCarry>[1]> = {}) => findCarry(src, { profiles: ['ci-reuse/free-tests'], head: HEAD, tree: TREE, wtree: clean, ...over });
const why = (v: ReturnType<typeof findCarry>) => (v.ok ? 'ok' : v.reason);
describe('findCarry', () => {
// Value: protects=a green verified run at this exact tree carries, with a receipt naming it; fails_when=a verified run is ignored or its count is lost;
// why_new=GSTA-258 piece 1; seam=lib/run-evidence.ts verifyRun; no_claim=GitHub's API shape (ghRunDeps)
test('the newest verified run at HEAD carries, and the receipt names its run and tree', () => {
const v = findCarry(source([run(4)], { 4: record(4) }), { profiles: ['ci-reuse/free-tests'], head: HEAD, tree: TREE, wtree: clean, inventory: () => ['test/a.test.ts', 'test/b.test.ts'] });
expect(v.ok).toBe(true);
if (!v.ok) return;
expect(v.tests).toBe(40);
expect(v.runs.map(e => e.run_id)).toEqual([4]);
expect(v.receipt).toMatch(/^ci-carry tree=bbbbbbbbbbbb tests=40 runs=4\/1 profiles=ci-reuse\/free-tests heads=aaaaaaaaaaaa verified=/);
});
// Value: protects=A2, a carry binds to the working-tree fingerprint before and after; fails_when=a dirty tree or a tree edited during the lookup carries
test('a dirty working tree, or one that changes while runs are read, never carries', () => {
expect(why(carry(source([run(4)], { 4: record(4) }), { wtree: () => 'c'.repeat(40) }))).toBe('dirty_tree');
expect(why(carry(source([run(4)], { 4: record(4) }), { wtree: () => null }))).toBe('dirty_tree');
let reads = 0;
expect(why(carry(source([run(4)], { 4: record(4) }), { wtree: () => (reads++ ? 'c'.repeat(40) : TREE) }))).toBe('tree_changed');
});
// Value: protects=A3, a lane carries only what CI provably ran; fails_when=a tree.json missing an inventory file still carries
test('the inventory must be covered by the runs\' tree.json files', () => {
const src = source([run(4)], { 4: record(4) });
const v = carry(src, { inventory: () => ['test/a.test.ts', 'test/c.test.ts'] });
expect(why(v)).toBe('inventory_uncovered');
expect(v.ok ? '' : v.detail).toContain('test/c.test.ts');
expect(why(carry(src, { inventory: () => ({ error: 'exited 1' }) }))).toBe('inventory_failed');
expect(why(carry(src, { inventory: () => [] }))).toBe('inventory_empty');
});
// Value: protects=fork runs, newer red or running runs, stale runs and empty runs never carry; fails_when=any of them is accepted
test('no run, a fork run, a newer red or running run, a stale run and a run with no tests are refused', () => {
expect(why(carry(source([])))).toBe('no_run');
expect(why(carry(source([run(4, { head_repository: { full_name: 'evil/gstack' } })], { 4: record(4) })))).toBe('no_verified_run');
const red = carry(source([run(4), run(6, { conclusion: 'failure' })], { 4: record(4), 6: record(6) }));
expect(why(red)).toBe('no_verified_run');
expect(red.ok ? '' : red.detail).toContain('run 6: run_not_success; run 4: newer_run_vetoes');
expect(why(carry(source([run(4), run(6, { status: 'in_progress', conclusion: null })], { 4: record(4) })))).toBe('no_verified_run');
expect(why(carry(source([run(4)], { 4: record(4) }, [{ name: 'free-tests', conclusion: 'success', completed_at: '2026-10-30T00:00:00Z' }])))).toBe('no_verified_run');
expect(why(carry(source([run(4)], { 4: record(4, { files: [] }) })))).toBe('no_tests');
expect(why(carry(source([run(4)], { 4: record(4) }), { profiles: ['nope'] }))).toBe('no_profile');
});
});
describe('waitForRun', () => {
const clockAt = () => { let t = 0; return { clock: () => t, sleep: async (ms: number) => { t += ms; } }; };
// Value: protects=the bounded wait reports green, red with failing jobs, still running and none; fails_when=it waits forever or calls a red run green
test('green, red with its failing jobs, still running at the deadline, and none', async () => {
const o = (runs: ApiRun[], jobs?: ApiJob[]) => waitForRun(source(runs, {}, jobs).deps, { workflow: 'free-tests.yml', events: PROFILE.events, head: HEAD, timeoutMs: 60_000, intervalMs: 20_000, ...clockAt() });
expect((await o([run(4)])).state).toBe('green');
const red = await o([run(4, { conclusion: 'failure' })], [{ name: 'free-tests', conclusion: 'failure' }, { name: 'typecheck', conclusion: 'success' }]);
expect(red).toMatchObject({ state: 'red', failing: ['free-tests'] });
expect((await o([run(4, { status: 'in_progress', conclusion: null })])).state).toBe('running');
expect((await o([])).state).toBe('none');
});
});
describe('ship_lanes_from config and the ci_carried receipt field', () => {
// Value: protects=A3, the mapping names real profiles and one inventory command; fails_when=a typo or a label-shaped entry validates
test('ship_lanes_from validates its profiles and inventory', () => {
const profiles = { 'ci-reuse/free-tests': PROFILE };
expect(validateConfig({ run_profiles: profiles, ship_lanes_from: { 'bun run test': { profiles: ['ci-reuse/free-tests'], inventory: 'bun run list' } } })).toEqual([]);
expect(shipLanesErrors({ 'bun run test': { profiles: ['nope'], inventory: 'x' } }, ['ci-reuse/free-tests']).map(e => e.message)).toEqual(['"nope" is not a run_profiles name']);
expect(shipLanesErrors({ 'bun run test': { profiles: ['ci-reuse/free-tests'], inventory: 'a\nb' } }, ['ci-reuse/free-tests'])[0]!.path).toBe('ship_lanes_from["bun run test"].inventory');
expect(shipLanesErrors({ 'bun run test': { profiles: ['ci-reuse/free-tests'] , inventory: 'x', label: 'free' } }, ['ci-reuse/free-tests'])[0]!.message).toContain('unknown key');
expect(shipLanesErrors(['bun run test'], [])[0]!.path).toBe('ship_lanes_from');
});
// Value: protects=the receipt and its SHIP_RECEIPT line name the carried runs; fails_when=ci_carried is dropped or malformed entries validate
test('the receipt records ci_carried and its summary line names each run', () => {
const receipt = { schema_version: 1 as const, head: HEAD, base: 'main', ci_carried: [{ lane: 'free', run: 41, attempt: 2, tree: TREE }] };
expect(validateReceipt(receipt)).toEqual([]);
expect(receiptSummaryLine(receipt)).toContain('ci_carried=free:41/2');
expect(validateReceipt({ ...receipt, ci_carried: [{ lane: 'free', run: '41' }] }).length).toBeGreaterThan(0);
});
});
+141
View File
@@ -0,0 +1,141 @@
/**
* gstack-evidence reuse and CI-sourced rows (GSTA-258 pieces 1-2, design fixes A4 and A5).
*
* run --reuse: a lane that passed at this exact working tree, command and execution identity never runs again;
* the newest matching row decides (a newer failure vetoes), the reused row keeps its original timestamp and the
* max age applies to it. record-ci: a lane CI carried is an explicit CI-sourced row for a run it verified itself, never a fictitious local run.
*/
import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { findFilesBySuffix, gitIn } from './helpers/scratch-repo';
import { runBin } from './helpers/run-bin';
setDefaultTimeout(60_000);
const EVIDENCE = path.join(import.meta.dir, '..', 'bin', 'gstack-evidence');
let home: string;
let repo: string;
let counter: string;
const run = (args: string[]) => runBin(EVIDENCE, args, { cwd: repo, env: { GSTACK_HOME: home } });
const ledger = () => findFilesBySuffix(path.join(home, 'projects'), '-evidence.jsonl')[0]!;
const rows = (): any[] => fs.readFileSync(ledger(), 'utf8').trim().split('\n').map(l => JSON.parse(l));
const rewrite = (edit: (r: any[]) => any[]) => fs.writeFileSync(ledger(), edit(rows()).map(r => JSON.stringify(r)).join('\n') + '\n');
const executions = () => (fs.existsSync(counter) ? fs.readFileSync(counter, 'utf8').length : 0);
const lane = (pass: number, fail = 0) => `printf x >> '${counter}'; printf ' ${pass} pass\\n ${fail} fail\\nRan ${pass + fail} tests across 1 file. [1.00ms]\\n' >&2; exit ${fail ? 1 : 0}`;
const wtree = () => runBin(path.join(import.meta.dir, '..', 'bin', 'gstack-wtree'), [], { cwd: repo }).stdout.trim();
beforeEach(() => {
home = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-reuse-home-'));
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-reuse-repo-'));
counter = path.join(home, 'executions');
gitIn(repo, 'init -q -b main');
fs.writeFileSync(path.join(repo, 'src.txt'), 'v1\n');
gitIn(repo, 'add src.txt');
gitIn(repo, 'commit -q -m init');
});
afterEach(() => {
fs.rmSync(home, { recursive: true, force: true });
fs.rmSync(repo, { recursive: true, force: true });
});
describe('gstack-evidence run --reuse (A5)', () => {
// Value: protects=a lane that passed at this tree never reruns, and the reuse is visible; fails_when=--reuse runs the suite again, or reports a pass it never recorded;
// why_new=GSTA-258 piece 2; seam=the ledger and the wtree fingerprint; no_claim=cross-machine reuse (CI carries that)
test('a pass at the same tree, command and identity is reused with its original timestamp and count', () => {
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
const first = rows().at(-1);
const again = run(['run', '--reuse', '--label', 'unit', '--', lane(4)]);
expect(again.status).toBe(0);
expect(executions()).toBe(1);
expect(again.stderr).toContain(`gstack-evidence: reused label=unit tree=${wtree().slice(0, 12)} from=${first.ts}`);
expect(again.stderr).toMatch(/gstack-evidence: recorded label=unit exit=0 tests_ran=4 log=\S+/);
const copy = rows().at(-1);
expect(copy.ts).toBe(first.ts);
expect(copy.reused_from).toBe(first.ts);
expect(copy.identity).toBe(first.identity);
expect(typeof first.identity).toBe('string');
expect(run(['check', '--label', 'unit']).stdout).toContain('EVIDENCE: FRESH label=unit exit=0 tests_ran=4');
});
// Value: protects=only the newest matching row decides; fails_when=an older pass is reused under a newer failure on the same tree
test('a newer failure on the same tree vetoes an older pass; without --reuse nothing is reused', () => {
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(run(['run', '--label', 'unit', '--', lane(3, 1)]).status).toBe(1);
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(3, 1)]).status).toBe(1);
expect(executions()).toBe(3);
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(executions()).toBe(5);
});
// Value: protects=reuse binds to the exact command, tree, count and identity; fails_when=a changed command, an edited file, a zero-run or unknown row, or another runtime reuses
test('another command, an edited tree, a zero-run or unknown count, or another identity runs again', () => {
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(5)]).status).toBe(0);
expect(executions()).toBe(2);
fs.writeFileSync(path.join(repo, 'src.txt'), 'v2\n');
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(5)]).status).toBe(0);
expect(executions()).toBe(3);
rewrite(r => r.map(x => ({ ...x, tests_ran: 0 })));
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(5)]).status).toBe(0);
expect(executions()).toBe(4);
rewrite(r => r.map(x => ({ ...x, tests_ran: 'unknown' })));
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(5)]).status).toBe(0);
expect(executions()).toBe(5);
rewrite(r => r.map(x => ({ ...x, identity: 'f'.repeat(64) })));
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(5)]).status).toBe(0);
expect(executions()).toBe(6);
});
// Value: protects=the max age applies to the original pass, never to a refreshed copy; fails_when=reuse makes an old pass look new
test('a pass older than --max-age is not reused, and a reused copy keeps the age check honest', () => {
expect(run(['run', '--label', 'unit', '--', lane(4)]).status).toBe(0);
rewrite(r => r.map(x => ({ ...x, ts: new Date(Date.now() - 30 * 3600_000).toISOString() })));
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(executions()).toBe(2);
rewrite(r => r.map(x => ({ ...x, ts: new Date(Date.now() - 2 * 3600_000).toISOString() })));
expect(run(['run', '--reuse', '--max-age', '1', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(executions()).toBe(3);
rewrite(r => r.map(x => ({ ...x, ts: new Date(Date.now() - 3 * 3600_000).toISOString() })));
expect(run(['run', '--reuse', '--label', 'unit', '--', lane(4)]).status).toBe(0);
expect(executions()).toBe(3);
expect(run(['check', '--label', 'unit', '--max-age', '2']).stdout).toContain('EVIDENCE: STALE label=unit');
});
});
describe('gstack-evidence record-ci and carried (A4)', () => {
const ciRow = () => {
const tree = wtree();
return { ts: new Date(Date.now() - 600_000).toISOString(), label: 'free', command: 'bun run test', cmd_sha256: createHash('sha256').update('bun run test').digest('hex'), exit: 0, duration_s: 0, commit: gitIn(repo, 'rev-parse HEAD').trim(), tree, dirty: false, wtree: tree, tests_ran: 900, source: 'ci', runs: ['41/2'], profiles: ['ci-reuse/free-tests'] };
};
// Value: protects=a carried lane is an explicit CI row the FRESH check accepts by source, and the receipt lists it only at its own tree;
// fails_when=a check cannot tell a CI row from a local run, or a stale carry from an earlier head reaches ci_carried
test('a CI-sourced row is FRESH for its exact command, and carried lists it only at its tree', () => {
expect(run(['run', '--label', 'unit', '--', lane(2)]).status).toBe(0);
fs.appendFileSync(ledger(), JSON.stringify(ciRow()) + '\n');
const chk = run(['check', '--label', 'free', '--expect-cmd', 'bun run test', '--max-age', '24']);
expect(chk.status).toBe(0);
expect(chk.stdout).toContain('EVIDENCE: FRESH label=free exit=0 tests_ran=900');
expect(chk.stdout).toContain('source=ci runs=41/2');
expect(JSON.parse(run(['carried', '--json']).stdout)).toEqual([{ lane: 'free', run: 41, attempt: 2, tree: wtree() }]);
fs.writeFileSync(path.join(repo, 'src.txt'), 'v2\n');
gitIn(repo, 'commit -qam two');
expect(JSON.parse(run(['carried', '--json']).stdout)).toEqual([]);
});
// Value: protects=record-ci finds and verifies the CI run itself, so no caller supplies run ids, a test count or a tree (/ship pass 1 red-team#1);
// fails_when=the forged call from that finding writes a FRESH carried row; seam=no origin, so the base's ship_lanes_from is unreadable
test('record-ci takes no run facts from its caller and records nothing without a verified run', () => {
const forged = run(['record-ci', '--label', 'free', '--runs', '1/1', '--profiles', 'anything', '--tree', wtree(), '--tests', '999', '--finished', new Date().toISOString(), '--', 'bun run test']);
expect(forged.status).toBe(2);
expect(forged.stderr).toContain('usage: gstack-evidence record-ci --label <L> --base <base> -- <command>');
const unverified = run(['record-ci', '--label', 'free', '--base', 'main', '--', 'bun run test']);
expect(unverified.status).toBe(1);
expect(unverified.stderr).toMatch(/^CARRY_NONE: config_unavailable: /m);
expect(findFilesBySuffix(path.join(home, 'projects'), '-evidence.jsonl')).toEqual([]);
});
});
+45
View File
@@ -0,0 +1,45 @@
/**
* A fake gh for the CI-carry tests (GSTA-258): serves the Actions run, jobs, artifact and download reads
* lib/run-evidence.ts ghRunDeps makes from a world file, and answers every other gh call with nothing.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface FakeWorld {
runs: Array<Record<string, unknown> & { id: number; run_attempt: number; head_sha: string; path: string }>;
jobs: Record<string, Array<{ name: string; conclusion: string | null; completed_at?: string }>>;
trees: Record<string, Record<string, unknown>>;
}
/** The stub body for shipFixture's gh(): run this file with the world beside it. */
export const fakeGhScript = (worldFile: string) => `exec bun --no-env-file --config=/dev/null '${import.meta.path}' '${worldFile}' "$@"`;
if (import.meta.main) {
const [worldFile, ...a] = process.argv.slice(2);
const world: FakeWorld = fs.existsSync(worldFile!) ? JSON.parse(fs.readFileSync(worldFile!, 'utf8')) : { runs: [], jobs: {}, trees: {} };
const lines = (list: unknown[]) => process.stdout.write(list.map(x => JSON.stringify(x)).join('\n') + (list.length ? '\n' : ''));
if (a[0] === 'api') {
const jq = a.indexOf('--jq');
const p = a.filter((x, i) => i > 0 && x !== '--paginate' && i !== jq && i !== jq + 1)[0] ?? '';
const [route, query = ''] = p.split('?');
const q = new URLSearchParams(query);
let m: RegExpExecArray | null;
if ((m = /\/actions\/runs\/(\d+)$/.exec(route!))) {
const run = world.runs.find(r => r.id === Number(m![1]));
if (!run) { process.stderr.write('HTTP 404'); process.exit(1); }
process.stdout.write(JSON.stringify(run));
} else if ((m = /\/actions\/workflows\/([^/]+)\/runs$/.exec(route!))) {
lines(world.runs.filter(r => r.head_sha === q.get('head_sha') && r.path === `.github/workflows/${m![1]}`));
} else if ((m = /\/actions\/runs\/(\d+)\/attempts\/\d+\/jobs$/.exec(route!))) {
lines(world.jobs[m[1]!] ?? []);
} else if ((m = /\/actions\/runs\/(\d+)\/artifacts$/.exec(route!))) {
lines(world.trees[m[1]!] ? [{ name: q.get('name'), size_in_bytes: 200, expired: false }] : []);
} else { process.stderr.write(`fake gh: no route for ${p}`); process.exit(1); }
} else if (a[0] === 'run' && a[1] === 'download') {
const tree = world.trees[a[2]!];
if (!tree) process.exit(1);
const dir = a[a.indexOf('--dir') + 1]!;
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'tree.json'), JSON.stringify(tree));
}
}
+98
View File
@@ -0,0 +1,98 @@
/**
* CI carry and the remote gate (GSTA-258 A): after a verified CI run carries part of the tests stage on a small
* machine, the files left over can all be ones the remote gate never runs (gstack's paid evals). The gate itself then
* reports tests=0 (PREGATE_REMOTE_PAID_ONLY) and `clear --run` refuses paid_only, so the obligation could never clear.
* The carried run clears it instead, through remote_gate_runs and its inventory check. The run API is a fake.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import type { PregateFile } from '../lib/headless-artifacts';
import { buildContext } from '../lib/pregate/context';
import { clearDecision, runPregate, type RunCheck } from '../lib/pregate/index';
import type { ApiRun, RunProfile } from '../lib/run-evidence';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
const LANE = 'pregate.json/remote_gate';
const NOW = new Date('2026-11-02T12:00:00Z');
const LOCAL_OS = process.platform === 'darwin' ? 'macOS' : 'Linux';
const PROFILE: RunProfile = { workflow: 'free-tests.yml', events: ['pull_request', 'workflow_dispatch'], jobs: ['unit'], artifact: 'tested-tree', os: LOCAL_OS };
const PAID = 'test/nightly/retry-heavy.test.js';
const FREE = ['test/retry.test.js', 'test/util.test.js'];
function fixture(): FixtureRepo {
const repo = makeFixtureRepo({ pregate: { ...BASE_PREGATE, remote_gate: 'node scripts/remote-gate.js' } });
repos.push(repo);
commit(repo, { 'lib/util.js': 'exports.add = (a, b) => a + b + 0;\n', 'lib/retry.js': 'exports.MAX_RETRIES = 3;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\nexports.touched = true;\n' }, 'touch two subjects');
return repo;
}
/** Run 100 of free-tests.yml at HEAD, green, its tree.json listing `files`; the gate's inventory is FREE and its skipped command names PAID. */
function runCheck(repo: FixtureRepo, files: string[]): RunCheck {
const head = repo.git('rev-parse', 'HEAD');
const api: ApiRun = { id: 100, run_attempt: 1, status: 'completed', conclusion: 'success', event: 'workflow_dispatch', head_sha: head, path: '.github/workflows/free-tests.yml', created_at: '2026-11-02T10:00:00Z', updated_at: '2026-11-02T10:30:00Z', repository: { full_name: 'fx/fx' }, head_repository: { full_name: 'fx/fx' } };
return {
repo: 'fx/fx', profiles: { 'ci-reuse/free-tests': PROFILE }, now: NOW,
gateRuns: { profiles: ['ci-reuse/free-tests'], inventory: 'INVENTORY', skipped: 'SKIPPED' },
inventory: (command, _tree, selected) => (command === 'SKIPPED' ? (selected ?? []).filter(f => f === PAID) : FREE),
deps: {
run: () => api, runsForHead: (_wf, sha) => (sha === head ? [api] : []), jobs: () => [{ name: 'unit', conclusion: 'success' }],
artifact: () => ({ run_id: 100, run_attempt: 1, event: 'workflow_dispatch', sha: head, head_sha: head, tree: repo.git('rev-parse', 'HEAD^{tree}'), os: [LOCAL_OS], files, tests: files.length * 4 }),
commitTree: sha => repo.git('rev-parse', `${sha}^{tree}`),
},
} as RunCheck;
}
async function carried(repo: FixtureRepo, check: RunCheck): Promise<{ file: PregateFile; progress: string[] }> {
const progress: string[] = [];
const loaded = buildContext({ repoRoot: repo.work, base: 'main', policyFrom: 'base', allowRepoCommands: true, env: { ...process.env, ...repo.env }, progress: l => progress.push(l), cores: 4, testBackend: 'local' });
if (!('ctx' in loaded)) throw new Error('fixture context refused');
loaded.ctx.fromRun = { ref: '100', check };
return { file: await runPregate(loaded.ctx, { stage: 'tests' }), progress };
}
const gate = (file: PregateFile) => file.requires_remote?.find(r => r.lane === LANE);
describe.skipIf(process.platform === 'win32')('CI carry clears the remote gate on a small machine', () => {
// Value: protects=a carried run whose remainder is only gate-skipped (paid) files leaves no obligation nothing can clear;
// fails_when=the remainder stays a remote_gate obligation that the gate reports tests=0 for and clear --run refuses paid_only;
// why_new=GSTA-258 A, the PREGATE_REMOTE_PAID_ONLY dead end seen at 0fba5533 (112 paid files after run 41554380563); seam=runPregate with a fake run API
test('the carried run clears the gate when only skipped files remain, and the receipt counts them', async () => {
const repo = fixture();
const check = runCheck(repo, FREE);
const { file, progress } = await carried(repo, check);
const obligation = gate(file)!;
expect(obligation.tests).toEqual([PAID]);
expect(clearDecision({ ...file, requires_remote: [{ ...obligation, receipt: null }] }, LANE, { runs: ['100'], evidence: check }, repo.work)).toMatchObject({ refused: 'PREGATE_RECEIPT_INVALID', message: expect.stringContaining('reason=paid_only') });
expect([obligation.receipt, obligation.cleared_by]).toEqual([expect.stringMatching(new RegExp(`^remote-gate tree=${file.tree.slice(0, 12)} tests=8 skipped=1 runs=100/1 `)), 'ci-carry']);
expect(progress).toContain(`CI_CARRY: cleared ${LANE} ${obligation.receipt}`);
expect(file.verdict).toBe('requires-remote cleared');
}, 120_000);
// Value: protects=the carry clears the gate only under clear --run's own checks; fails_when=a run that misses an inventory file clears it
test('a run that misses a gate inventory file leaves the gate uncleared and says why', async () => {
const repo = fixture();
const { file, progress } = await carried(repo, runCheck(repo, ['test/util.test.js']));
expect(gate(file)?.receipt).toBeNull();
expect(progress).toContainEqual(expect.stringMatching(new RegExp(`^CI_CARRY: ${LANE} stays uncleared \\(REUSE: refused reason=inventory_uncovered`)));
}, 120_000);
// Value: protects=when the base declares remote_gate_runs, the uncleared gate names the CI route first and the VM only as an opt-in;
// fails_when=the obligation sends an agent to the paid VM while a CI run at the same tree would clear it; why_new=GSTA-258 A, plan choice F1
test('with remote_gate_runs on the base, an uncleared gate names the CI route first and the VM as the opt-in', async () => {
const repo = makeFixtureRepo({ pregate: { ...BASE_PREGATE, remote_gate: 'node scripts/remote-gate.js', run_profiles: { 'ci-reuse/free-tests': PROFILE }, remote_gate_runs: { profiles: ['ci-reuse/free-tests'], inventory: 'echo test/retry.test.js' } } });
repos.push(repo);
commit(repo, { 'lib/retry.js': 'exports.MAX_RETRIES = 3;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\nexports.touched = 2;\n' }, 'touch retry');
const loaded = buildContext({ repoRoot: repo.work, base: 'main', policyFrom: 'base', allowRepoCommands: true, env: { ...process.env, ...repo.env }, progress: () => {}, cores: 4, testBackend: 'local' });
if (!('ctx' in loaded)) throw new Error('fixture context refused');
const file = await runPregate(loaded.ctx, { stage: 'tests' });
expect(gate(file)?.receipt).toBeNull();
const detail = file.checks.find(c => c.id === 'lanes')?.detail ?? '';
expect(detail).toContain(`a verified CI run at this tree clears it: push, then gstack-ci-carry wait --base main and rerun gstack-pregate --allow-repo-commands`);
expect(detail.indexOf('gstack-ci-carry wait')).toBeLessThan(detail.indexOf('opt in to the VM: gstack-pregate --remote --spend-cap <usd> --allow-repo-commands'));
loaded.ctx.ciCarry = false;
const off = (await runPregate(loaded.ctx, { stage: 'tests' })).checks.find(c => c.id === 'lanes')?.detail ?? '';
expect(off).not.toContain('gstack-ci-carry wait');
expect(off).toContain('run it, then gstack-pregate clear --remote pregate.json/remote_gate');
}, 120_000);
});
@@ -54,6 +54,15 @@ describe('pregate secrets: source context and diff integrity', () => {
expect([...parseAddedLines(diff)]).toEqual([['a.ts', ['one', '++two']], ['b.env', ['three']]]);
});
test('a Python call that reads key.json is code; the same key name holding a literal key still fails (GST-1064)', () => {
expect(repo({ 'attach.py': 'import json\nkey = json.loads(files.get("key.json", ""))\n' }).secrets().status).toBe('pass');
const fake = 'Zx9Q' + 'w8Er7Ty6Ui5Op4As3Df2';
const r = repo({ 'attach.py': `key = json.loads(files.get("key.json")) or "${fake}"\n`, '.env': `KEY=${fake}\n` }).secrets();
expect(r.status).toBe('fail');
expect(r.lines.join('\n')).toContain('attach.py');
expect(r.lines.join('\n')).toContain('.env');
});
test('a TS object property that reads a credential at runtime is code, not a secret (#3211)', () => {
const f = repo({ 'app.ts': 'const cfg = {\n token: credentials.token,\n apiKey: settings.apiKey,\n};\n' });
expect(f.secrets().status).toBe('pass');
+47
View File
@@ -106,6 +106,53 @@ describe("env.kv: a function call assigned to a credential name is code (#2899)"
);
});
describe("env.kv: a name-shaped literal inside a call is a file or dict key, not a secret (GST-1064)", () => {
const FAKE_KEY = "Zx9Qw8Er7Ty6Ui5Op4As3Df2";
const FAKE_STRIPE_TAIL = "4eC39HqLyjWDarjtT1zdp7dc";
table(
"env.kv",
"MEDIUM",
[
String.raw`key = json.loads(files.get("key.json", ""))`,
String.raw`token = resp.json().get("access_token")`,
String.raw` session = store.load('session.cookie')`,
String.raw`secret = vault.read("client_secret")`,
],
[
String.raw`key = "${FAKE_KEY}"`,
String.raw`KEY=${FAKE_KEY}`,
String.raw`key = json.loads("sk_live_${FAKE_STRIPE_TAIL}")`,
String.raw`token = resp.json().get("access_token", "${FAKE_KEY}")`,
String.raw`password = os.getenv("DB_PASSWORD", "hunter2.Zx9Qw8")`,
String.raw`key = json.loads(files.get("key.json")) or "${FAKE_KEY}"`,
String.raw`password = os.getenv("DB_PASSWORD", "correct-horse-battery-staple")`,
String.raw`api_key = decrypt("prod-signing-key")`,
String.raw`token = decrypt("correct_horse_battery_staple")`,
String.raw`key = decrypt("qwhzxkvbnmplor")`,
String.raw`SECRET_KEY = os.getenv("SECRET_KEY", "orange_monkey_pass")`,
String.raw`password = os.environ.get("DB_PASSWORD", "purple_walrus")`,
String.raw`token = resp.json().get("access_token", "orange_monkey_pass")`,
String.raw`key = derive_secret("tigers.horse")`,
String.raw`password = os.getenv("DB_PASSWORD", "qwhzxkvbnmplor.txt")`,
String.raw`password = make_password("orange_monkey_pass")`,
String.raw`password = bcrypt.hashpw("orange_monkey_pass", salt)`,
String.raw`token = jwt.encode("purple_walrus_pie", payload)`,
String.raw`api_key = settings.Secret("correct_horse_battery")`,
String.raw`password = hash_password("purple_walrus")`,
String.raw`password=hash_password("purple_walrus")`,
String.raw`+password = hash_password( "purple_walrus" )`,
String.raw`password = hash_password("purple_walrus").encode()`,
String.raw`password = hash_password("purple_walrus") # seed user`,
String.raw`password = config.get("db_password") or ("orange_monkey_pass")`,
String.raw`password = config.get("db_password", ("orange_monkey_pass"))`,
String.raw`password = login(user, ("purple_walrus"))`,
String.raw`password = load_cfg("app.yaml"); login("purple_walrus")`,
String.raw`password = login(user, str("purple_walrus"))`,
String.raw`password = encrypt(str("orange_monkey_pass"))`,
],
);
});
describe("internal.hostname: per-machine config filenames are not hosts (#2962, #2740)", () => {
table(
"internal.hostname",
+2 -1
View File
@@ -66,6 +66,7 @@ const NO_LOSS_REPLAYS = {
'GST-508+R51': "wave review-53cfb5bfe011 merge of #3744 into main: lib/review-depth.ts keeps both admitted changes, the GST-508 no-loss replay of review_exclude_paths (Arm V 33 of 41 blockers vs R 33 of 41, p_NI 0.0279; GStack Minder drive handoffs/GSTA-202/noloss-results.md) and the R51 risk-tier re-score with GST-850's dispatch-superset no-loss check: excluded paths sit beside the added and removed line maps, and the exclusion filter keeps the publishes-docs rule",
'GST-508+GST-464': "wave review-53cfb5bfe011 merge of #3744 into main: ship/sections/review-army.md and adversarial.md carry the GST-508 no-loss-replayed exclude pathspec placeholders inside the brief bodies (handoffs/GSTA-202/noloss-results.md) and the GST-464 lead-instruction edits outside the cut spans, which its brief-identity no-loss check admitted",
'GST-986': "GST-986 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/adversarial.md is byte-identical to the bodies at the GST-257 pin; only the lead's Step 11 instruction naming the targeted record's path changed, outside the cut spans",
'GST-942': "GST-942 dispatch-superset no-loss check: the rmSync rule no longer stops at the first ), a matching rm( rule is added, and both read a file's changed lines joined for a delete split across lines, so a recursive delete raises security on more diffs and never on fewer; over the last 100 first-parent main commits to 3d99c2249 (62,531 changed lines) the line rules match all 170 lines the old rule matched plus 19 more, and no commit changes depth, class or risk class; reviewer text is unchanged",
'GST-911-redos': "GST-911 ReDoS fix verdict-identity no-loss check (wave review-e7b551ebd865): SVG_REFUSED's url() entry, rewritten to run in linear time, refuses exactly the strings #3838's pattern refused (test/review-risk-redos.test.ts: 60,000 seeded strings and the hand-picked edges, and every SVG blob in the repository's history, with zero differences); SVG_MARKUP now refuses a DOCTYPE naming any document but W3C's SVG DTD (test/review-planner-misreads.test.ts), which only keeps more SVGs code and so drops no reviewer; GST-911's no-loss replay holds unchanged; reviewer text is unchanged",
'GST-911': "GST-911 no-loss replay (GSTA-249/13): the planner over the last 100 first-parent main commits to f5cb6ec1e, origin/main's code against this branch, gate scan fixed at 0, changes no commit's depth, class, risk classes, targeted classes or SCOPE_AUTH; the three misreads (a drawing-only SVG, a session-named data file, a bare base) are proven on fixtures (test/review-planner-misreads.test.ts), where every SVG that can run script stays code with the security class; reviewer text is unchanged",
'GST-508+R51+GST-911': "wave review-53cfb5bfe011 merge of main 3d99c2249 (#3848) into #3852: lib/review-depth.ts keeps GST-508's review_exclude_paths (no-loss replay, GStack Minder drive handoffs/GSTA-202/noloss-results.md), R51 with GST-850's dispatch-superset no-loss check, and GST-911's SVG drawings with GST-911-redos (no-loss replay: 0 of 100 main commits changed, reports/GSTA-249-911-decisions.tsv; test/review-planner-misreads.test.ts, test/review-risk-redos.test.ts); lib/review-risk.ts is main's GST-911 bytes. Merged-head checks (handoffs/GSTA-202/merge-3d99c22-checks.md): Arm P on unit 3593 differs only in the exclude pathspec and the disclosure line, the same brief diff as before the merge, and GST-911 panel unit f5cb6ec1e plans, dispatches and renders byte-identical briefs under 3d99c2249 and the merged head",
@@ -95,7 +96,7 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
'review/specialists/targeted.md': { sha: '7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5', replay: 'GST-257' },
'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' },
'lib/review-depth.ts': { sha: 'd76a9afcfadfe0a95b15428c04e6f7a88c6b0fa75591201c45f023fec2bb90db', replay: 'GST-508+R51+GST-911+GST-967' },
'lib/review-risk.ts': { sha: '126eff0f1de6982e03dc24d8017c67e6807c91dfa518b18d2db3aa910aac911f', replay: 'GST-508+R51+GST-911' },
'lib/review-risk.ts': { sha: '08464b78a723ee463ca3a28d0abbc4eb8f4f512192061c703743707b66b5a311', replay: 'GST-942' },
};
/**
+24 -5
View File
@@ -5,7 +5,7 @@
*/
import { describe, expect, test } from 'bun:test';
import { SVG_REFUSED, svgDrawing } from '../lib/review-depth';
import { DATA_LINE, MONEY_LINE, SECRET_LINE, SECRET_PATH, SECURITY_LINE, matchRisk } from '../lib/review-risk';
import { DATA_LINE, MONEY_LINE, SECRET_LINE, SECRET_PATH, SECURITY_JOINED, SECURITY_LINE, matchRisk } from '../lib/review-risk';
const L = 100_000;
const ms = (f: () => unknown) => { const t = performance.now(); f(); return performance.now() - t; };
@@ -18,7 +18,7 @@ describe('linear time on hostile input', () => {
// or on a line of repeated anchors; seam=every pattern in SECURITY_LINE, DATA_LINE, SECRET_LINE, MONEY_LINE and SECRET_PATH
const hostile = [
run('rm -', 'r'), run('rm -', 'f'), run('rm -r', 'r'), run('rm -f', 'a'), run('rm\t-', 'r'), run('rm ', ' '), run('rm -', 'r', '1'), run('rm -', 'rf', '_'), run('', 'rm -r '),
run('', 'rmSync('), run('rmSync(', ' '), run('rmSync(', 'recursive '), run('rmSync(', 'recursive: '),
run('', 'rmSync('), run('rmSync(', ' '), run('rmSync(', 'recursive '), run('rmSync(', 'recursive: '), run('', '.rm('), run('.rm(', ' '), run('.rm(', 'recursive: '), run('', 'rm('), run('rm(', ' '), run('rmSync(', ' \n'),
run('shell', ' '), run('shell:', ' '), run('shell=', ' '), run('', '; '), run(';', ' '), run('; eval', ' '), run('eval', ' '), run('', ';eval '),
run('execSync', ' '), run('child_process.exec', ' '), run('os.system', ' '), run('new', ' '), run('new Function', ' '),
run('spawn(', ' '), run('Bun.spawn(', ' '), run('Bun.spawn([', ' '), run('innerHTML', ' '),
@@ -27,7 +27,7 @@ describe('linear time on hostile input', () => {
run('', 'a_'), run('', 'aU'), run('a', 'A'), run('spend', '_'), run('new Anthropic', ' '), run('.messages.create', ' '),
run('$', 'A_'), run('${', 'A_'), run('', '$A_'), run('process.env.', 'A_'), run('process.env[', ' '), run('os.environ[', ' '), run('os.getenv(', ' '), run('ENV[', ' '),
];
test.each(Object.entries({ SECURITY_LINE, DATA_LINE, SECRET_LINE, MONEY_LINE, SECRET_PATH: [SECRET_PATH] }))('every %s pattern finishes under 100 ms on each 100k-character hostile line', (_n, patterns) => {
test.each(Object.entries({ SECURITY_LINE, SECURITY_JOINED, DATA_LINE, SECRET_LINE, MONEY_LINE, SECRET_PATH: [SECRET_PATH] }))('every %s pattern finishes under 100 ms on each 100k-character hostile line', (_n, patterns) => {
const slow = patterns.flatMap(r => hostile.map(([label, s]) => [r.source.slice(0, 50), label, Math.round(ms(() => r.test(s)))] as const)).filter(([, , t]) => t >= 100);
expect(slow).toEqual([]);
}, 120_000);
@@ -57,13 +57,13 @@ describe('the rm rule matches what R51 matched', () => {
expect(lines.filter(s => r51.test(s)).length).toBeGreaterThan(1000);
expect(edges.filter(s => rm.test(s))).toEqual(['rm -rf /', 'rm -fr x', 'rm -rvf x', 'rm -rf-x', 'rm -rrf', 'rm -ffr', 'rm -rfr', 'rm\t\t-rf', 'sudo rm -rf "$DIR"']);
});
test('the rmSync rule matches what R51 matched on 60,000 seeded short lines, and padding past its window still raises security', () => {
test('the rmSync rule matches every line R51 matched on 60,000 seeded short lines, and padding past its window still raises security', () => {
x = 898;
const r51Sync = /\brmSync\s*\([^)]*recursive\s*:\s*true/;
const rmSync = SECURITY_LINE.find(r => r.source.startsWith(String.raw`\brmSync`))!;
const parts = ['rmSync(', 'rmSync (', 'xrmSync(', 'recursive', ':', ' ', 'true', ')', 'a', 'recursive: true', 'rmSync', '(', '\t'];
const lines = Array.from({ length: 60_000 }, () => Array.from({ length: next(9) + 1 }, () => parts[next(parts.length)]).join(''));
expect(lines.filter(s => rmSync.test(s) !== r51Sync.test(s))).toEqual([]);
expect(lines.filter(s => r51Sync.test(s) && !rmSync.test(s))).toEqual([]);
expect(lines.filter(s => r51Sync.test(s)).length).toBeGreaterThan(1000);
expect(security('fs.rmSync(dir, { recursive: true, force: true });')).toBe(true);
expect(security('fs.rmSync(' + 'a'.repeat(480) + ', { recursive: true })')).toBe(true);
@@ -71,6 +71,25 @@ describe('the rm rule matches what R51 matched', () => {
expect(security('fs.rmSync(dir, /*' + 'x'.repeat(5_000_000) + '*/ { recursive: true });')).toBe(true);
expect(security('fs.rmSync(dir, { force: true });')).toBe(false);
});
test('a recursive rm or rmSync whose first argument is a call, or whose arguments hold a ), raises security (GST-942)', () => {
// Value: protects=a recursive delete written the common way keeps the security class; fails_when=the rule stops at the first
// ) inside the first argument or a comment, or skips the async fs.rm; seam=matchRisk on one added line
const recursive = ['fs.rmSync(path.join(dir, "x"), { recursive: true, force: true });', 'fs.rmSync(dir, /* ) */ { recursive: true });',
'fs.rmSync(path.resolve(root, name), { recursive: true });', 'await fs.promises.rm(path.join(dir, "x"), { recursive: true });',
'await fs.rm(dir, { recursive: true })', "await rm(dir, { recursive: true, force: true }); // import { rm } from 'node:fs/promises'",
'fs.rm(' + 'a'.repeat(600) + ', { force: true })'];
expect(recursive.filter(l => !security(l))).toEqual([]);
expect(['fs.rm(path.join(dir, "x"), { force: true });', 'farm(x, { recursive: true });', 'confirm(x, { recursive: true });'].filter(security)).toEqual([]);
});
test('a recursive delete a formatter split across lines raises security, read over the joined lines (GST-942 review)', () => {
// Value: protects=a Prettier-formatted rmSync or fs.promises.rm keeps the security class; fails_when=the delete rules read
// only one line at a time; seam=matchRisk over a file's added lines
const split = (lines: string[]) => matchRisk({ codePaths: ['src/clean.ts'], added: { 'src/clean.ts': lines }, removed: {}, scopeAuth: false }).has('security');
expect(split([' fs.rmSync(path.join(root, "build-output-cache"), {', ' recursive: true,', ' force: true,', ' });'])).toBe(true);
expect(split([' await fs.promises.rm(', ' path.join(root, "cache"),', ' { recursive: true },', ' );'])).toBe(true);
expect(split([' fs.rmSync(', ' dir,', ' /*' + 'x'.repeat(510) + '*/', ' { recursive: true },', ' );'])).toBe(true);
expect(split([' fs.rmSync(path.join(root, "file"), {', ' force: true,', ' });'])).toBe(false);
});
});
describe('svgDrawing in linear time', () => {
+194
View File
@@ -0,0 +1,194 @@
/**
* CI-carried lanes end to end (GSTA-258 pieces 1, 2 and 4; design fixes A1-A4): gstack-ship-preflight, gstack-ship-release
* finish, push --early and accept, gstack-pregate's automatic --from-run and gstack-ci-carry, against a fixture repository
* whose gh is a fake serving one Actions world, so nothing reaches GitHub.
*/
import { afterEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fakeGhScript, type FakeWorld } from './helpers/fake-gh-runs';
import { findFilesBySuffix } from './helpers/scratch-repo';
import { lineOf, PREFLIGHT, RELEASE, shipFixture, type ShipFx } from './helpers/ship-bin-fixture';
setDefaultTimeout(120_000);
const BIN = path.dirname(RELEASE);
const fixtures: ShipFx[] = [];
afterEach(() => { while (fixtures.length) fixtures.pop()!.cleanup(); });
const FREE = "printf x >> ../free-runs; printf ' 3 pass\\n 0 fail\\nRan 3 tests across 1 file. [1.00ms]\\n' >&2";
const UNIT = "printf x >> ../unit-runs; printf ' 2 pass\\n 0 fail\\nRan 2 tests across 1 file. [1.00ms]\\n' >&2";
const PROFILE = { workflow: 'free.yml', events: ['pull_request', 'workflow_dispatch'], jobs: ['suite'], artifact: 'tested-tree', os: 'Linux' };
const BASE = {
'.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [FREE]: { profiles: ['ci/free'], inventory: "printf 'test/a.test.ts\\ntest/b.test.ts\\n'" } } }, null, 2) + '\n',
'.github/workflows/free.yml': 'name: Free\non: [pull_request, workflow_dispatch]\njobs:\n suite:\n runs-on: ubuntu-latest\n steps:\n - run: bun test\n',
};
const fx = () => { const f = shipFixture(BASE); fixtures.push(f); return f; };
const runs = (f: ShipFx, name: string) => { const p = path.join(f.root, name); return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').length : 0; };
const at = (minutesAgo: number) => new Date(Date.now() - minutesAgo * 60_000).toISOString();
/** One run of free.yml at HEAD (id 7 unless named), green with a tree.json naming HEAD's tree, unless overridden. */
function world(f: ShipFx, over: { run?: Record<string, unknown>; tree?: Record<string, unknown>; others?: Array<Record<string, unknown>>; jobs?: Array<{ name: string; conclusion: string | null }> } = {}): void {
const head = f.git('rev-parse', 'HEAD').stdout.trim();
const tree = f.git('rev-parse', 'HEAD^{tree}').stdout.trim();
const run = { id: 7, run_attempt: 1, status: 'completed', conclusion: 'success', event: 'workflow_dispatch', head_sha: head, path: '.github/workflows/free.yml', created_at: at(20), run_started_at: at(20), updated_at: at(5), repository: { full_name: 'fx/fx' }, head_repository: { full_name: 'fx/fx' }, ...over.run };
const w: FakeWorld = {
runs: [run, ...((over.others ?? []).map(o => ({ ...run, ...o })))] as FakeWorld['runs'],
jobs: { 7: (over.jobs ?? [{ name: 'suite', conclusion: 'success' }]).map(j => ({ ...j, completed_at: at(5) })), 9: [{ name: 'suite', conclusion: 'failure', completed_at: at(1) }] },
trees: { 7: { run_id: 7, run_attempt: 1, event: 'workflow_dispatch', sha: head, tree, files: ['test/a.test.ts', 'test/b.test.ts'], tests: 12, os: 'Linux', ...over.tree } },
};
const file = path.join(f.root, 'world.json');
fs.writeFileSync(file, JSON.stringify(w));
f.gh(fakeGhScript(file));
}
const preflight = (f: ShipFx, lanes = [`free=${FREE}`, `unit=${UNIT}`]) => f.run(PREFLIGHT, ['--tests-only', '--base', 'main', ...lanes.flatMap(l => ['--lane', l])]);
const lane = (out: string, label: string) => out.split('\n').find(l => l.startsWith(`LANE: ${label} `)) ?? '';
describe('Step 5 lanes: CI carry and the tree cache', () => {
// Value: protects=a lane the base maps is carried from CI at this exact tree and never runs; any other lane runs once and is reused at the same tree;
// fails_when=the free suite reruns locally beside a green CI run, or a passed lane reruns at an unchanged tree; why_new=GSTA-258 pieces 1-2; seam=fake gh, gstack-evidence ledger
test('the mapped lane is carried from the green run; the other lane runs once, then is reused', () => {
const f = fx();
world(f);
const first = preflight(f);
expect(first.status).toBe(0);
expect(lane(first.stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=12');
expect(lane(first.stdout, 'unit')).toMatch(/^LANE: unit pass exit=0 tests_ran=2 log=\S+$/);
expect([runs(f, 'free-runs'), runs(f, 'unit-runs')]).toEqual([0, 1]);
const again = preflight(f);
expect(lane(again.stdout, 'unit')).toMatch(/^LANE: unit pass exit=0 tests_ran=2 log=\S+ reused=\S+$/);
expect([runs(f, 'free-runs'), runs(f, 'unit-runs')]).toEqual([0, 1]);
const chk = f.run(path.join(BIN, 'gstack-evidence'), ['check', '--label', 'free', '--expect-cmd', FREE]);
expect(chk.stdout).toContain('source=ci runs=7/1');
});
// Value: protects=A2/A3 and verifyRun's refusals: a dirty tree, an uncovered inventory, a fork run and a newer red run never carry, and a label alone never maps;
// fails_when=any of them carries; seam=lib/ci-carry.ts findCarry through runLanes
test('a dirty tree, an uncovered inventory, a fork run, a newer red run or a changed command runs the lane here', () => {
const f = fx();
const cases: Array<[string, () => void, string[]?]> = [
['dirty_tree', () => { world(f); f.write({ 'src/b.ts': 'export const b = 3;\n' }); }],
['inventory_uncovered', () => { f.git('checkout', '--', 'src/b.ts'); world(f, { tree: { files: ['test/a.test.ts'] } }); }],
['fork_run', () => world(f, { run: { head_repository: { full_name: 'evil/fx' } } })],
['newer_run_vetoes', () => world(f, { others: [{ id: 9, conclusion: 'failure', created_at: at(2), run_started_at: at(2) }] })],
];
let expected = 0;
for (const [reason, setup] of cases) {
setup();
const out = preflight(f, [`free=${FREE}`]).stdout;
expect([reason, out]).toEqual([reason, expect.stringContaining('CI_CARRY: none label=free (')]);
expect(out).toContain(reason);
expect(lane(out, 'free')).toMatch(/^LANE: free pass exit=0 tests_ran=3 /);
expected += lane(out, 'free').includes('reused=') ? 0 : 1;
expect(runs(f, 'free-runs')).toBe(expected);
}
world(f);
const other = preflight(f, [`free=${FREE} `]).stdout;
expect(other).not.toContain('CI_CARRY');
expect(lane(other, 'free')).not.toContain('carried');
});
// Value: protects=a CI-sourced ledger row exists only for a run gstack-evidence record-ci found and verified itself (/ship pass 1 red-team#1);
// fails_when=caller-supplied run ids, test counts or a tree make a FRESH carried row, or an unmapped command or a missing run records one; seam=fake gh
test('record-ci verifies the run itself: forged facts, an unmapped command or no run at HEAD record nothing', () => {
const f = fx();
const ev = (args: string[]) => f.run(path.join(BIN, 'gstack-evidence'), args);
const forged = ev(['record-ci', '--label', 'free', '--runs', '1/1', '--profiles', 'anything', '--tree', f.git('rev-parse', 'HEAD^{tree}').stdout.trim(), '--tests', '999', '--finished', at(1), '--', FREE]);
expect(forged.status).toBe(2);
world(f, { run: { head_sha: '0'.repeat(40) } });
const noRun = ev(['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([noRun.status, noRun.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: no_run: /m)]);
world(f);
const unmapped = ev(['record-ci', '--label', 'free', '--base', 'main', '--', 'bun run test']);
expect([unmapped.status, unmapped.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: not_mapped: /m)]);
expect(ev(['check', '--label', 'free', '--expect-cmd', FREE]).stdout).not.toContain('EVIDENCE: FRESH');
expect(JSON.parse(ev(['carried', '--json']).stdout)).toEqual([]);
const ok = ev(['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([ok.status, ok.stderr]).toEqual([0, expect.stringContaining('tests_ran=12 log=- source=ci runs=7/1 policy=origin/main@')]);
const row = JSON.parse(fs.readFileSync(findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl')[0]!, 'utf8').trim().split('\n').at(-1)!);
expect(row.policy).toMatch(/^origin\/main@[0-9a-f]{12}:\.gstack\/pregate\.json$/);
});
});
describe('finish, push --early and accept (A1)', () => {
const finishArgs = (f: ShipFx, reviewed: string, post: string) => ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--lane', `free=${FREE}`];
const snapshot = (f: ShipFx) => {
const out = path.join(f.root, 'post.json');
expect(f.run(path.join(BIN, 'gstack-docs-candidate'), ['snapshot', '--out', out, '--audit-id', 'a1', '--mode', 'edit', '--base', 'main', '--docs', 'README.md']).status).toBe(0);
return out;
};
const unattended = { GSTACK_SESSION_KIND: 'unattended' };
// Value: protects=finish never runs a mapped lane locally on the stamped tree: it defers it to accept and runs only pregate's preflight stage; accept carries
// it from the green run at the pushed head and reruns the pregate there; fails_when=the stamped tree reruns the suite, or accept trusts a run at another tree;
// why_new=design fix A1; seam=fake gh, gstack-evidence, gstack-pregate
test('finish defers the mapped lane; accept carries it from the green run at the pushed head', () => {
const f = fx();
world(f);
const reviewed = f.run(path.join(BIN, 'gstack-wtree'), []).stdout.trim();
expect(f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro']).status).toBe(0);
const post = snapshot(f);
const pregateFile = path.join(f.work, '.gstack/tmp/pregate.json');
const before = fs.existsSync(pregateFile) ? fs.readFileSync(pregateFile, 'utf8') : null;
const fin = f.run(RELEASE, finishArgs(f, reviewed, post));
expect(fin.stdout).toMatch(/^EVIDENCE: DEFERRED label=free \(/m);
expect(lineOf(fin.stdout, 'PREGATE')).toMatch(/^PREGATE: preflight (pass|warn) on the stamped tree/);
expect(fs.existsSync(pregateFile) ? fs.readFileSync(pregateFile, 'utf8') : null).toBe(before);
expect(fin.stdout).toContain('gstack-ship-release accept --base main --lane');
expect(fin.status).toBe(0);
expect(runs(f, 'free-runs')).toBe(0);
const notPushed = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `free=${FREE}`, '--wait', '0']);
expect(notPushed.stdout).toContain('is not origin/feat: run gstack-ship-release push first');
const push = f.run(RELEASE, ['push', '--branch', 'feat', '--early', '--base', 'main'], unattended);
expect(lineOf(push.stdout, 'SECRETS')).toMatch(/^SECRETS: (pass|warn) \(early push\)$/);
expect(lineOf(push.stdout, 'PUSH')).toMatch(/^PUSH: pushed /);
world(f, { run: { status: 'in_progress', conclusion: null } });
const running = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `free=${FREE}`, '--wait', '0']);
expect(running.status).toBe(3);
expect(running.stdout).toContain('CI_WAIT: running ci/free run=7/1');
world(f, { run: { conclusion: 'failure' }, jobs: [{ name: 'suite', conclusion: 'failure' }] });
const red = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `free=${FREE}`, '--wait', '0']);
expect(red.stdout).toContain('STOP: free.yml run 7 failed (suite)');
world(f);
const ok = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `free=${FREE}`, '--wait', '0']);
expect(ok.stdout).toContain('CI_WAIT: green ci/free run=7/1');
expect(lane(ok.stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=12');
expect(lineOf(ok.stdout, 'EVIDENCE')).toMatch(/^EVIDENCE: FRESH label=free exit=0 tests_ran=12 .* source=ci runs=7\/1$/);
expect(ok.stdout).toContain('CI_CARRY: accepted ci-carry tree=');
expect(ok.stdout).toContain('NEXT: Step 18');
expect(ok.status).toBe(0);
expect(runs(f, 'free-runs')).toBe(0);
const carried = JSON.parse(f.run(path.join(BIN, 'gstack-evidence'), ['carried', '--json']).stdout);
expect(carried).toEqual([{ lane: 'free', run: 7, attempt: 1, tree: f.git('rev-parse', 'HEAD^{tree}').stdout.trim() }]);
});
});
describe('gstack-pregate tests stage and gstack-ci-carry', () => {
// Value: protects=the tests stage carries CI at HEAD's clean tree on its own, and --no-ci-carry or a dirty tree turns it off; fails_when=pregate reruns what CI ran, or carries a dirty tree;
// why_new=GSTA-258 piece 1; seam=fake gh; no_claim=a small machine's remote gate obligation (covered by from-run's tests)
test('the tests stage accepts the run on its own, and --no-ci-carry or a dirty tree does not', () => {
const f = fx();
world(f);
const pregate = (extra: string[] = []) => f.run(path.join(BIN, 'gstack-pregate'), ['--stage', 'tests', '--allow-repo-commands', '--base', 'main', ...extra]);
expect(pregate().stderr).toMatch(/^CI_CARRY: accepted ci-carry tree=[0-9a-f]{12} tests=12 runs=7\/1 profiles=ci\/free /m);
expect(pregate(['--no-ci-carry']).stderr).not.toContain('CI_CARRY');
f.write({ 'scratch.txt': 'x\n' });
expect(pregate().stderr).toContain('CI_CARRY: none (dirty_tree');
});
// Value: protects=the CLI finds and waits on the same evidence; fails_when=find or wait accepts a red run or prints no verdict
test('gstack-ci-carry find and wait', () => {
const f = fx();
world(f);
const cli = (args: string[]) => f.run(path.join(BIN, 'gstack-ci-carry'), args);
const found = cli(['find', '--base', 'main']);
expect([found.status, found.stdout]).toEqual([0, expect.stringMatching(/^CI_CARRY: accepted ci-carry tree=/)]);
const mapped = cli(['find', '--base', 'main', '--command', FREE, '--allow-repo-commands']);
expect(mapped.status).toBe(0);
expect(cli(['find', '--base', 'main', '--command', 'bun run test', '--allow-repo-commands']).stdout).toContain('CI_CARRY: none (not_mapped');
expect(cli(['wait', '--base', 'main', '--timeout', '0']).stdout).toContain('CI_WAIT: green ci/free run=7/1');
world(f, { run: { conclusion: 'failure' }, jobs: [{ name: 'suite', conclusion: 'failure' }] });
const red = cli(['wait', '--base', 'main', '--timeout', '0']);
expect([red.status, red.stdout.trim()]).toEqual([1, 'CI_WAIT: red ci/free run=7/1 failing=suite']);
expect(cli(['bogus']).status).toBe(2);
});
});
@@ -0,0 +1,4 @@
{
"bytes": 1463,
"why": "ship: CI-carry, finish/accept split and early push (GSTA-258 piece A)"
}