fix(ship): record-ci reuses its own CI row at the same tree, run and selection (GST-1795) (#4127)

* fix(ship): record-ci reuses its own CI row at the same tree, run and selection (GST-1795)

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

* test(windows): curate ship-record-ci-reuse like its ship-bin-fixture siblings (run 42382484796)

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

* fix: pre-landing review fixes

record-ci reuse: the tamper table gains a row for a CI row whose clean flag is false, so dropping reusableCiRow's dirty guard fails a test.

Review-Finding: bin/gstack-evidence:730:completeness-gaps reader=core
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

record-ci reuse stands only on the remainder runs its CI row was written from: a <lane>.skipped row newer than the CI row (a zero-test rerun record-ci refused) vetoes reuse instead of passing on exit 0 alone.

Review-Finding: p1/outside-voice#1 reader=outside-voice
Review-Finding: p1/adversarial-native#1 reader=adversarial-native
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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-07 15:44:35 +00:00
1 parent 52d6c46957
commit cefcb20a06
8 files changed
+208 -7

No files matched your search

+38 -2
View File
@@ -712,6 +712,35 @@ function reused(file: string, label: string, cmdSha: string, maxAgeHours: number
return true;
}
/** record-ci's record line, the one lib/ci-carry.ts recordCiResult reads. */
const recordedCiLine = (r: EvidenceRecord): string =>
`gstack-evidence: recorded label=${r.label} exit=0 tests_ran=${r.tests_ran}${r.tests_unit ? ` unit=${r.tests_unit}` : ""} log=- source=ci runs=${r.runs!.join(",")} policy=${r.policy}${r.skipped_here ? ` skipped_here=${r.skipped_here.files.length} tests_ran_here=${r.skipped_here.tests_ran}${r.skipped_here.not_owed ? ` not_owed_here=${r.skipped_here.not_owed.length}` : ""}` : ""}`;
/**
* record-ci's reuse (GST-1795): the label's newest ledger row stands only when it is the CI row this record-ci would write again
* (every `want` field equal: commit, clean tree, command, verified runs, profiles, jobs, base policy, count, unit, not-owed reason),
* it grades FRESH, it carries no test-lane exception, and its per_file remainder is the same files, each file's newest local run at
* this tree written before that row (the run the row was validated from) and a pass under this runtime. Anything else records afresh.
* The caller has already verified the run itself live.
*/
function reusableCiRow(label: string, want: Partial<EvidenceRecord>, remainder: string[], perFile: string | undefined): EvidenceRecord | null {
let rows: EvidenceRecord[] = [];
try { rows = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { return null; }
const same = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
const at = rows.findLastIndex((x) => x.label === label);
const r = rows[at];
if (!r || r.source !== "ci" || r.exception || r.dirty !== false || r.tree !== want.wtree) return null;
if (!Object.entries(want).every(([k, v]) => same(r[k as keyof EvidenceRecord], v))) return null;
if (gradeRecord(r, { maxAgeHours: DEFAULT_MAX_AGE_HOURS, allowPaths: [], wtreeNow: want.wtree }).verdict !== "FRESH") return null;
if (!same(r.skipped_here?.files ?? [], remainder)) return null;
const identity = identityDigest();
const passedHere = (file: string) => {
const i = rows.findLastIndex((x) => x.label === `${label}.skipped` && x.source === undefined && x.cmd_sha256 === sha256(perFileCommand(perFile!, [file])) && x.wtree === want.wtree);
return i >= 0 && i < at && rows[i]!.exit === 0 && rows[i]!.identity === identity;
};
return remainder.every(passedHere) ? r : null;
}
/**
* `record-ci`: carry a lane from CI. Its command must be a key of the base branch's ship_lanes_from; the run is found and
* verified here (lib/ci-carry.ts) at HEAD's clean tree, so no caller supplies run ids, a test count or a tree. Never runs the lane;
@@ -772,6 +801,14 @@ async function cmdRecordCi(argv: string[]): Promise<number> {
return 0;
}
if (!v.ok) return none(v.reason, `${v.detail}${v.reason === "inventory_uncovered" && !atHead.length && elsewhere.length ? `; test-lane exception ${elsewhere.join(", ")} binds another head, not HEAD ${head.slice(0, 12)}` : ""}`, v.diagnosis);
const runs = v.runs.map((r) => `${r.run_id}/${r.run_attempt}`);
const prior = v.excepted?.length ? null : reusableCiRow(label, { commit: head, wtree: tree, cmd_sha256: sha256(command), runs, profiles: m.profiles, policy: loaded.label, jobs: m.jobs, tests_ran: v.tests, tests_unit: v.unit, not_owed: v.notOwed }, v.remainder ?? [], m.per_file);
if (prior) {
console.error(recordedCiLine(prior));
if (prior.not_owed) console.error(`gstack-evidence: not owed: ${prior.not_owed}`);
console.error(`CI_RECORD_REUSED: label=${label} runs=${runs.join(",")} tree=${tree.slice(0, 12)} policy=${loaded.label} (the ledger's CI row for this exact tree, run and selection; nothing ran here)`);
return 0;
}
let exception: BoundLaneException | undefined;
const grant = atHead.find((g) => g.approval === v.exceptedBy);
if (v.excepted?.length && grant) {
@@ -826,13 +863,12 @@ async function cmdRecordCi(argv: string[]): Promise<number> {
console.error(`gstack-evidence record-ci: ledger setup failed (${e?.message ?? e}); nothing recorded`);
return 1;
}
const runs = v.runs.map((r) => `${r.run_id}/${r.run_attempt}`);
const rec: EvidenceRecord = {
ts: new Date(v.runs.map((r) => r.finished_at).sort()[0]!).toISOString(), label, command, cmd_sha256: sha256(command), exit: 0, duration_s: 0,
commit: head, tree, dirty: false, wtree: tree, tests_ran: testsRan, source: "ci", runs, profiles: m.profiles, policy: loaded.label, ...(m.jobs ? { jobs: m.jobs } : {}), ...(v.unit ? { tests_unit: v.unit } : {}), ...(v.notOwed ? { not_owed: v.notOwed } : {}), ...(here ? { skipped_here: here } : {}), ...(exception ? { exception } : {}),
};
appendJsonl(paths.file, rec, { mode: 0o600 });
console.error(`gstack-evidence: recorded label=${label} exit=0 tests_ran=${testsRan}${v.unit ? ` unit=${v.unit}` : ""} log=- source=ci runs=${runs.join(",")} policy=${loaded.label}${here ? ` skipped_here=${here.files.length} tests_ran_here=${here.tests_ran}${here.not_owed ? ` not_owed_here=${here.not_owed.length}` : ""}` : ""}`);
console.error(recordedCiLine(rec));
if (v.notOwed) console.error(`gstack-evidence: not owed: ${v.notOwed}`);
if (exception) console.error(`TEST_LANE_EXCEPTION: ${testLaneExceptionText(exception)}`);
return 0;
+1
View File
@@ -0,0 +1 @@
- **`gstack-ship-release accept` no longer reruns a CI carry's local remainder that Step 5 already ran at the same tree.** /ship's Step 5 and accept both call `gstack-evidence record-ci`, which ran the lane's `per_file` remainder (the inventory files CI skipped entirely) each time, so on harness-evals #315 a remainder file of about 7 minutes ran twice and accept's record-ci hit the 600 s `ci_record_timeout_s`. After verifying the run live, record-ci now reuses the label's newest ledger row when it is the CI row it would write again: same commit, clean tree, command, run id and attempt, profiles, jobs, base policy, covered count and remainder files, FRESH, no test-lane exception, and each remainder file's newest local run at this tree written before that row and a pass under this runtime. It prints `CI_RECORD_REUSED: label=<lane> runs=<id>/<attempt> tree=<tree12> policy=<policy>`, and accept's lane line ends `(reused the CI record at tree <tree12>)`. Any mismatch, a newer row for the label, or a run that is no longer green records afresh or refuses as before. No timeout or coverage change. See docs/pregate.md#record-ci-reuse. (GST-1795)
+16
View File
@@ -1270,6 +1270,22 @@ them (`ci_carried=<lane>:<run>/<attempt>`). Each `record-ci` gets the base
policy's [`ci_record_timeout_s`](ship-policy.md#ci-record-timeout) (600 s by
default, 3600 s at most), and accept prints it as `CI_RECORD_TIMEOUT:`.
<a id="record-ci-reuse"></a>**record-ci reuses its own row at the same tree (GST-1795).** Step 5's
carry and accept both call `record-ci`, so at an unchanged pushed tree the
second call would run the `per_file` remainder again. Instead, after it
verifies the run live, `record-ci` reuses the label's newest ledger row when
that row is the CI row it would write again: the same commit, clean tree,
command, verified runs (id and attempt), profiles, jobs, base policy, covered
count, unit and not-owed reason, FRESH within 24 hours, with no test-lane
exception, and the same remainder files, each file's newest `<lane>.skipped`
run at this tree written before that row and a pass under this runtime (a
remainder run after the CI row, such as a zero-test rerun record-ci refused,
vetoes reuse). It runs nothing here, prints
`CI_RECORD_REUSED: label=<lane> runs=<id>/<attempt> tree=<tree12> policy=<policy>`
after its record line, and accept's lane line ends `(reused the CI record at
tree <tree12>)`. Any difference, a newer row for the label, or a run that is no
longer green records afresh (or refuses) as before.
<a id="accept-local-tests"></a>**When accept's final pregate runs the tests here (GST-1606).** The final
pregate's tests stage carries only under a base run profile whose `os` matches
this machine and whose workflow holds a lane this machine runs. A lane carried
+3 -2
View File
@@ -206,7 +206,7 @@ export function importedTestLines(cwd: string, missing: string[], candidates: st
* not-owed reason, or the last CARRY_NONE reason. A lane the cleared remote gate stood in for (--remote-gate-stands-in, GST-1688)
* reads as its receipt, its count and why the CI carry refused.
*/
export function recordCiResult(stderr: string): { runs?: string; tests?: string; files?: boolean; notOwed?: string; here?: string; exception?: string; none?: string; diagnosis?: string[]; remoteGate?: string; standIn?: string } {
export function recordCiResult(stderr: string): { runs?: string; tests?: string; files?: boolean; notOwed?: string; here?: string; exception?: string; reused?: string; none?: string; diagnosis?: string[]; remoteGate?: string; standIn?: string } {
const lines = stderr.split('\n');
const last = lines.findLast(x => x.startsWith('gstack-evidence: recorded label='));
const gateRow = last && / tests_ran=(\d+) log=- source=remote_gate receipt=(.+)$/.exec(last);
@@ -220,7 +220,8 @@ export function recordCiResult(stderr: string): { runs?: string; tests?: string;
const got = / tests_ran=(\d+|n\/a)( unit=files)? log=- source=ci runs=(\S+)/.exec(lines[at]!);
const here = / skipped_here=(\d+) tests_ran_here=(\S+)( not_owed_here=\d+)?$/.exec(lines[at]!);
const notOwed = lines.slice(at + 1).find(x => x.startsWith('gstack-evidence: not owed: '))?.slice('gstack-evidence: not owed: '.length);
return { runs: got?.[3], tests: got?.[1], files: !!got?.[2], ...(notOwed ? { notOwed } : {}), ...(here ? { here: `skipped_here=${here[1]} file(s) tests_ran_here=${here[2]}${here[3] ?? ''}` } : {}), ...(exception ? { exception } : {}) };
const reused = lines.slice(at + 1).map(x => /^CI_RECORD_REUSED: .* tree=([0-9a-f]+) /.exec(x)?.[1]).find(Boolean);
return { runs: got?.[3], tests: got?.[1], files: !!got?.[2], ...(notOwed ? { notOwed } : {}), ...(here ? { here: `skipped_here=${here[1]} file(s) tests_ran_here=${here[2]}${here[3] ?? ''}` } : {}), ...(exception ? { exception } : {}), ...(reused ? { reused } : {}) };
}
/** The per_file runner command for `files`: `{files}` replaced by the paths, each anchored with ./ (Bun reads a bare relative path as a substring filter) and single-quoted for bash. */
+2 -2
View File
@@ -204,7 +204,7 @@ export function carryLane(cwd: string, base: string, l: { label: string; command
const rec = bin(cwd, 'gstack-evidence', ['record-ci', '--label', l.label, '--base', base, '--remote-gate-stands-in', '--', l.command], timeoutS * 1000);
const got = recordCiResult(rec.stderr);
if (rec.status === 0 && got.remoteGate) return { label: l.label, exit: 0, testsRan: got.tests!, log: '-', remoteGate: got.remoteGate, standIn: got.standIn ?? 'not carried' };
if (rec.status === 0 && got.runs) return { label: l.label, exit: 0, testsRan: got.files ? `${got.tests} (files)` : got.tests!, log: '-', carried: got.runs, ...(got.notOwed ? { notOwed: got.notOwed } : {}), ...(got.here ? { here: got.here } : {}), ...(got.exception ? { exception: got.exception } : {}) };
if (rec.status === 0 && got.runs) return { label: l.label, exit: 0, testsRan: got.files ? `${got.tests} (files)` : got.tests!, log: '-', carried: got.runs, ...(got.notOwed ? { notOwed: got.notOwed } : {}), ...(got.here ? { here: got.here } : {}), ...(got.exception ? { exception: got.exception } : {}), ...(got.reused ? { reused: got.reused } : {}) };
return { label: l.label, exit: null, testsRan: 'unknown', log: '-', carryNone: got.none ?? `record_failed: ${firstLine(rec.stderr)}`, ...(got.diagnosis ? { diagnosis: got.diagnosis } : {}) };
}
@@ -245,7 +245,7 @@ export async function runLanes(cwd: string, list: { label: string; command: stri
/** Step 5's lane verdict: a lane passes on its runner count, never exit 0 alone; a carried lane names its CI runs. */
export function laneLine(l: LaneResult): { line: string; ok: boolean } {
if (l.notOwed) return { line: `LANE: ${l.label} not owed: ${l.notOwed} run=${l.carried}`, ok: true };
if (l.carried) return { line: `LANE: ${l.label} carried run=${l.carried} tests_ran=${l.testsRan}${l.here ? ` ${l.here}` : ''}`, ok: true };
if (l.carried) return { line: `LANE: ${l.label} carried run=${l.carried} tests_ran=${l.testsRan}${l.here ? ` ${l.here}` : ''}${l.reused ? ` (reused the CI record at tree ${l.reused})` : ''}`, ok: true };
if (l.remoteGate) return { line: `LANE: ${l.label} satisfied by remote_gate receipt=${l.remoteGate}`, ok: true };
const zero = l.exit === 0 && l.testsRan === '0';
const state = l.exit !== 0 ? 'FAIL' : zero ? 'ZERO-RUN' : l.testsRan === 'unknown' ? 'pass (count unavailable)' : 'pass';
+4
View File
@@ -439,6 +439,10 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }>
file: 'test/ship-accept-remote-gate-lanes.test.ts',
reason: 'drives bin/gstack-ship-release, gstack-evidence and gstack-pregate (shebangs) through test/helpers/ship-bin-fixture.ts like test/ship-accept-same-tree-clears.test.ts; windows-latest fails every case at the first bin spawn (windows-scoped-shard, run 42302497169)',
},
{
file: 'test/ship-record-ci-reuse.test.ts',
reason: 'drives bin/gstack-evidence, gstack-ship-preflight and gstack-ship-release (shebangs) through test/helpers/ship-bin-fixture.ts like test/ship-accept-remote-gate-lanes.test.ts; windows-latest fails every case at the first bin spawn (windows-scoped-shard, run 42382484796)',
},
];
// Force-include overrides: files a WINDOWS_FRAGILE_PATTERNS regex excludes for
+2 -1
View File
@@ -599,7 +599,8 @@ describe('GST-1190: a lane carries the covered files and runs only the skipped r
expect(args(f).trim().split('\n')).toEqual(SIX.map(([p]) => './' + p));
const ci = rows(f).find(r => r.source === 'ci');
expect(ci).toMatchObject({ label: 'free', tests_ran: 1, tests_unit: 'files', skipped_here: { label: 'free.skipped', files: SIX.map(([p]) => p), tests_ran: 0, not_owed: SIX.map(([p]) => p) } });
expect(lane(preflight(f, [`free=${FREE}`]).stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=1 (files) skipped_here=6 file(s) tests_ran_here=0 not_owed_here=6');
expect(lane(preflight(f, [`free=${FREE}`]).stdout, 'free')).toMatch(/^LANE: free carried run=7\/1 tests_ran=1 \(files\) skipped_here=6 file\(s\) tests_ran_here=0 not_owed_here=6 \(reused the CI record at tree [0-9a-f]{12}\)$/);
expect(args(f).trim().split('\n')).toHaveLength(SIX.length);
});
test('GST-1314: a remainder that mixes a file that ran here with one that registers no test still refuses', () => {
+142
View File
@@ -0,0 +1,142 @@
/**
* GST-1795: `gstack-evidence record-ci` reuses its own CI row when the label's newest ledger row already records the same
* verified run at the same clean tree, profiles, policy and selection, instead of running the per_file remainder again.
* harness-evals #315: /ship's Step 5 carry ran the remainder (one file takes about 7 minutes), then accept's record-ci at the
* identical tree ran it again and hit the 600 s ci_record_timeout_s.
*/
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 EVIDENCE = path.join(path.dirname(RELEASE), 'gstack-evidence');
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 PER = "printf '%s\\n' {files} >> ../perfile-args; printf ' 1 pass\\n 0 fail\\nRan 1 tests across 1 file. [1.00ms]\\n' >&2";
const HOST_OS = ({ darwin: 'macOS', win32: 'Windows' } as Record<string, string>)[process.platform] ?? 'Linux';
const RUNNER = ({ darwin: 'macos-latest', win32: 'windows-latest' } as Record<string, string>)[process.platform] ?? 'ubuntu-latest';
const PROFILE = { workflow: 'free.yml', events: ['pull_request', 'workflow_dispatch'], jobs: ['suite'], artifact: 'tested-tree', os: HOST_OS };
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'", per_file: PER } } }, null, 2) + '\n',
'.github/workflows/free.yml': `name: Free\non: [pull_request, workflow_dispatch]\njobs:\n suite:\n runs-on: ${RUNNER}\n steps:\n - run: bun test\n`,
};
const at = (minutesAgo: number) => new Date(Date.now() - minutesAgo * 60_000).toISOString();
/** Run 7 of free.yml at HEAD, green, whose tree.json names HEAD's tree and lists test/b.test.ts as skipped entirely. */
function world(f: ShipFx, run: Record<string, unknown> = {}): void {
const head = f.git('rev-parse', 'HEAD').stdout.trim();
const tree = f.git('rev-parse', 'HEAD^{tree}').stdout.trim();
const r = { 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' }, ...run };
const w: FakeWorld = {
runs: [r] as FakeWorld['runs'],
jobs: { [r.id as number]: [{ name: 'suite', conclusion: r.conclusion === 'success' ? 'success' : 'failure', completed_at: at(5) }] },
trees: { [r.id as number]: { run_id: r.id, run_attempt: r.run_attempt, event: r.event, sha: head, tree, files: ['test/a.test.ts', 'test/b.test.ts'], skipped_files: ['test/b.test.ts'], tests: 12, os: HOST_OS } },
};
fs.writeFileSync(path.join(f.root, 'world.json'), JSON.stringify(w));
f.gh(fakeGhScript(path.join(f.root, 'world.json')));
}
function fx(): ShipFx { const f = shipFixture(BASE); fixtures.push(f); world(f); return f; }
const recordCi = (f: ShipFx) => f.run(EVIDENCE, ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
const perFileRuns = (f: ShipFx) => { const p = path.join(f.root, 'perfile-args'); return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).length : 0; };
const ledger = (f: ShipFx) => findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl')[0]!;
const rows = (f: ShipFx) => fs.readFileSync(ledger(f), 'utf8').trim().split('\n').map(l => JSON.parse(l));
/** Rewrite the newest row matching `pick`, as a stale or foreign record would read. */
function tamper(f: ShipFx, pick: (r: Record<string, any>) => boolean, edit: (r: Record<string, any>) => Record<string, any>): void {
const all = rows(f);
const i = all.findLastIndex(pick);
expect(i).toBeGreaterThanOrEqual(0);
all[i] = edit(all[i]);
fs.writeFileSync(ledger(f), all.map(r => JSON.stringify(r)).join('\n') + '\n');
}
const isCi = (r: Record<string, any>) => r.source === 'ci';
const isHere = (r: Record<string, any>) => r.label === 'free.skipped';
describe('record-ci reuses its own CI row at the same tree (GST-1795)', () => {
// Value: protects=a second record-ci at an identical tree, run, profile, policy and selection reuses the row instead of rerunning
// the per_file remainder, and names the run it reused; fails_when=harness-evals #315: accept reruns a ~7-minute remainder file
// that Step 5 already ran at the same tree and stops at ci_record_timeout_s; seam=fake gh, gstack-evidence record-ci
test('the second record-ci at the same tree reuses the row and runs nothing here', () => {
const f = fx();
const first = recordCi(f);
expect([first.status, perFileRuns(f)], first.stderr).toEqual([0, 1]);
const second = recordCi(f);
expect([second.status, perFileRuns(f)], second.stderr).toEqual([0, 1]);
expect(lineOf(second.stderr, 'CI_RECORD_REUSED')).toMatch(/^CI_RECORD_REUSED: label=free runs=7\/1 tree=[0-9a-f]{12} policy=origin\/main@/);
expect(second.stderr).toMatch(/^gstack-evidence: recorded label=free exit=0 tests_ran=1 unit=files log=- source=ci runs=7\/1 policy=\S+ skipped_here=1 tests_ran_here=1$/m);
expect(rows(f).filter(isCi)).toHaveLength(1);
});
// Value: protects=the /ship flow end to end: Step 5's carry ran the remainder, so accept at the pushed, identical tree reuses it and
// says so on its LANE line; fails_when=accept's record-ci runs the remainder again; seam=bin/gstack-ship-preflight, bin/gstack-ship-release accept
test('accept after the Step 5 carry at the same tree reuses it', () => {
const f = fx();
const pre = f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lane', `free=${FREE}`]);
expect([pre.status, perFileRuns(f)], pre.stdout + pre.stderr).toEqual([0, 1]);
expect(f.run(RELEASE, ['push', '--branch', 'feat', '--early', '--base', 'main'], { GSTACK_SESSION_KIND: 'unattended' }).status).toBe(0);
const out = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `free=${FREE}`, '--wait', '0']);
expect(out.stdout.split('\n').find(l => l.startsWith('LANE: free ')), out.stdout + out.stderr).toMatch(/^LANE: free carried run=7\/1 tests_ran=1 \(files\) skipped_here=1 file\(s\) tests_ran_here=1 \(reused the CI record at tree [0-9a-f]{12}\)$/);
expect(lineOf(out.stdout, 'EVIDENCE')).toStartWith('EVIDENCE: FRESH label=free');
expect(perFileRuns(f)).toBe(1);
});
// Value: protects=reuse fails closed: any field that would differ in a fresh record (tree, command, profiles, policy, runs, count,
// selection, the remainder's local runs and their runtime) records afresh and reruns the remainder; fails_when=a stale or foreign
// row is reused; seam=gstack-evidence record-ci against a tampered ledger
test.each([
['tree', isCi, (r: Record<string, any>) => ({ ...r, wtree: '0'.repeat(40) })],
['command', isCi, (r: Record<string, any>) => ({ ...r, cmd_sha256: '0'.repeat(64) })],
['profiles', isCi, (r: Record<string, any>) => ({ ...r, profiles: ['ci/other'] })],
['policy', isCi, (r: Record<string, any>) => ({ ...r, policy: 'origin/main@000000000000:.gstack/pregate.json' })],
['runs', isCi, (r: Record<string, any>) => ({ ...r, runs: ['6/1'] })],
['covered count', isCi, (r: Record<string, any>) => ({ ...r, tests_ran: 2 })],
['remainder selection', isCi, (r: Record<string, any>) => ({ ...r, skipped_here: { ...r.skipped_here, files: ['test/a.test.ts'] } })],
['age', isCi, (r: Record<string, any>) => ({ ...r, ts: at(25 * 60) })],
['exception', isCi, (r: Record<string, any>) => ({ ...r, exception: { approval: 'a-1', head: r.commit } })],
['clean flag', isCi, (r: Record<string, any>) => ({ ...r, dirty: true })],
['remainder runtime', isHere, (r: Record<string, any>) => ({ ...r, identity: '0'.repeat(64) })],
['remainder verdict', isHere, (r: Record<string, any>) => ({ ...r, exit: 1 })],
])('a mismatched %s records afresh', (_name, pick, edit) => {
const f = fx();
expect(recordCi(f).status).toBe(0);
tamper(f, pick, edit);
const again = recordCi(f);
expect([again.status, perFileRuns(f), lineOf(again.stderr, 'CI_RECORD_REUSED')], again.stderr).toEqual([0, 2, '']);
});
// Value: protects=the run's own verdict still decides: a run that turned red (a failed rerun attempt) carries nothing even with a
// matching row in the ledger; fails_when=the ledger row is reused without the live check; seam=fake gh, gstack-evidence record-ci
test('a run that is no longer green carries nothing, ledger row or not', () => {
const f = fx();
expect(recordCi(f).status).toBe(0);
world(f, { run_attempt: 2, conclusion: 'failure' });
const again = recordCi(f);
expect([again.status, perFileRuns(f), lineOf(again.stderr, 'CI_RECORD_REUSED')], again.stderr).toEqual([1, 1, '']);
expect(again.stderr).toMatch(/^CARRY_NONE: /m);
});
// Value: protects=reuse stands only on the remainder runs the CI row was written from, so a newer `.skipped` row (a zero-test rerun
// after a runtime change, which record-ci refuses as skipped_zero_run) never reads as the remainder's pass; fails_when=the newest
// exit-0 remainder row past the CI row satisfies reuse; seam=gstack-evidence record-ci against an appended ledger row
test('a remainder run newer than the CI row vetoes reuse', () => {
const f = fx();
expect(recordCi(f).status).toBe(0);
fs.appendFileSync(ledger(f), JSON.stringify({ ...rows(f).findLast(isHere)!, tests_ran: 0 }) + '\n');
const again = recordCi(f);
expect([again.status, perFileRuns(f), lineOf(again.stderr, 'CI_RECORD_REUSED')], again.stderr).toEqual([0, 2, '']);
});
// Value: protects=a newer label row (a local run of the lane after the carry) is what the ledger reads, so it vetoes reuse;
// fails_when=an older CI row is reused past a newer local row; seam=gstack-evidence run, record-ci
test('a newer row for the label vetoes reuse', () => {
const f = fx();
expect(recordCi(f).status).toBe(0);
expect(f.run(EVIDENCE, ['run', '--label', 'free', '--', FREE]).status).toBe(0);
const again = recordCi(f);
expect([again.status, perFileRuns(f), lineOf(again.stderr, 'CI_RECORD_REUSED')], again.stderr).toEqual([0, 2, '']);
});
});