v1.94.19.0 stamp: merge main 44471cff1f into #3932 (land)

This commit is contained in:
garrytan committed 2026-11-03 14:21:58 +00:00
commit 0e18bfa87f
88 files changed
+4006 -301

No files matched your search

+4 -2
View File
@@ -40,9 +40,11 @@ describe('findCarry', () => {
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.tests, v.unit]).toEqual([2, 'files']);
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=/);
expect(v.receipt).toMatch(/^ci-carry tree=bbbbbbbbbbbb files=2 runs=4\/1 profiles=ci-reuse\/free-tests heads=aaaaaaaaaaaa verified=/);
const whole = findCarry(source([run(4)], { 4: record(4) }), { profiles: ['ci-reuse/free-tests'], head: HEAD, tree: TREE, wtree: clean });
expect(whole.ok && [whole.tests, whole.unit, whole.receipt.split(' ')[2]]).toEqual([40, undefined, 'tests=40']);
});
// 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
+22
View File
@@ -223,6 +223,28 @@ describe('gstack-diff-scope', () => {
expect(scope.SCOPE_AUTH).toBe('true');
});
// Value: protects=auth matches as a word or path segment, so docs about authors stay out of the gate class;
// fails_when=the auth match is a bare substring again (author-spec.md, AUTHORS, authority.ts raise SCOPE_AUTH);
// why_new=GST-1065 (author-spec.md raised a docs bundle to gate class with four full readers); seam=bin/gstack-diff-scope
for (const file of ['docs/author-spec.md', 'AUTHORS', 'lib/authority.ts', 'docs/authored-by.md', 'src/coauthors.ts', 'AUTHORS.md']) {
test(`${file} is not an auth path`, () => {
expect(runScope(createRepo([file])).SCOPE_AUTH).toBe('false');
});
}
// Value: protects=real auth paths still raise SCOPE_AUTH after the word-boundary fix (negative controls for GST-1065);
// fails_when=the boundary drops a segment, a separator, a camelCase name, an authn/authz/oauth spelling or a glued token (auth0, basicauth, nextauth);
// why_new=GST-1065 and its review (the first word-only matcher dropped glued tokens the old glob caught); seam=bin/gstack-diff-scope
for (const file of ['src/auth.ts', 'app/auth/login.rb', 'lib/oauth.ts', 'src/oauth2/token.ts', 'lib/authn/check.go', 'src/authz.py', 'src/authService.ts',
'src/useAuth.ts', 'lib/OAuthClient.ts', 'lib/authentication.rb', 'lib/authorization_policy.rb', 'src/Authorize.tsx', 'config/AUTH_KEYS.md', 'pkg/auth-middleware.go',
'lib/auth0.ts', 'src/basicauth.go', 'src/authguard.ts', 'lib/reauth.ts', 'pages/api/nextauth.ts', 'src/authtoken.ts', 'lib/preauth.rb', 'src/HTTPAuth.ts',
'src/unauthenticated.ts', 'lib/reauthentication.ts', 'src/deauthorize.ts', 'app/oauth2client.py', 'api/HTTPAuthorization.ts',
'src/nextauthmiddleware.ts', 'src/reauthn.ts', 'src/noauthrequired.ts']) {
test(`${file} is an auth path`, () => {
expect(runScope(createRepo([file])).SCOPE_AUTH).toBe('true');
});
}
test('detects config via bun.lock (Bun v1.2+ text lockfile)', () => {
const dir = createRepo(['bun.lock']);
const scope = runScope(dir);
+3 -2
View File
@@ -109,7 +109,7 @@ describe('gstack-evidence run --reuse (A5)', () => {
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'] };
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'], policy: 'origin/main@000000000000:.gstack/pregate.json' };
};
// 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;
@@ -121,7 +121,8 @@ describe('gstack-evidence record-ci and carried (A4)', () => {
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() }]);
expect(JSON.parse(run(['carried', '--base', 'main', '--json']).stdout)).toEqual([{ lane: 'free', run: 41, attempt: 2, tree: wtree() }]);
expect(JSON.parse(run(['carried', '--base', 'release', '--json']).stdout)).toEqual([]);
fs.writeFileSync(path.join(repo, 'src.txt'), 'v2\n');
gitIn(repo, 'commit -qam two');
expect(JSON.parse(run(['carried', '--json']).stdout)).toEqual([]);
+1 -1
View File
@@ -872,7 +872,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
and `gstack-pregate sealed`, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -871,7 +871,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
and `gstack-pregate sealed`, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -878,7 +878,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch <branch-name>
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
and `gstack-pregate sealed`, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -880,7 +880,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch <branch-name>
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
and `gstack-pregate sealed`, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+28
View File
@@ -935,6 +935,34 @@ describe('scan', () => {
expect(r.err).toContain(`${SENTINEL.DETECT_REFUSED}: main (not a repository)`);
expect(r.code).toBe(1); // a base that cannot be diffed is a failed target, never a silent clean scan
});
test.skipIf(!POSIX)('--changed main reads origin/main over a stale local main (GST-1036)', () => {
// Value: protects=design review scans only the branch's frontend files; fails_when=a bare --changed base diffs against the stale local main
// and scans a stylesheet only upstream changed; why_new=GST-1036 (ship review-army passes a bare <base>); seam=bin/gstack-design-detect.ts
const bare = path.join(SANDBOX, 'stale-origin.git');
const work = path.join(SANDBOX, 'stale-work');
const other = path.join(SANDBOX, 'stale-other');
const log = path.join(SANDBOX, 'argv-stale.log');
try {
git(SANDBOX, 'clone', '-q', '--bare', REPO, bare);
git(SANDBOX, 'clone', '-q', bare, work);
git(SANDBOX, 'clone', '-q', bare, other);
for (const d of [work, other]) { git(d, 'config', 'user.email', 't@example.com'); git(d, 'config', 'user.name', 't'); }
fs.writeFileSync(path.join(other, 'src', 'styles.css'), '.hero { color: blue }\n');
git(other, 'commit', '-qam', 'upstream');
git(other, 'push', '-q', 'origin', 'HEAD:main');
git(work, 'fetch', '-q', 'origin');
git(work, 'checkout', '-q', '-b', 'feature', 'origin/main');
fs.writeFileSync(path.join(work, 'src', 'components', 'Card.tsx'), 'export const Card = () => <p />;\n');
git(work, 'commit', '-qam', 'change');
const r = run(['scan', '--changed', 'main', '--format', 'gstack'], { env: { IMPECCABLE_BIN: FAKE, IMPECCABLE_FAKE_LOG: log }, cwd: work });
const argv = JSON.parse(fs.readFileSync(log, 'utf-8').trim().split('\n')[0]).argv as string[];
expect(argv.slice(2).map(a => path.relative(fs.realpathSync(work), a)).sort()).toEqual(['src/components/Card.tsx']);
expect(r.err).toContain('gstack-design-detect: local main is behind origin/main; base is origin/main');
} finally {
for (const p of [bare, work, other, log]) fs.rmSync(p, { recursive: true, force: true });
}
});
});
describe('design-review REPORT_DIR agrees with the allow-list', () => {
+27 -1
View File
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { EXIT } from '../lib/headless-artifacts';
import { BUMP_NEVER_KEY_VERSION, CHANGELOG_FRAGMENTS_KEY_VERSION, DEFAULT_POLICY, POLICY_KEYS, POLICY_KEY_INTRODUCED, REVIEW_SCOPE_KEY_VERSION, globToRegExp, matchesAny, minimumReader, shipConsequences, validatePolicy } from '../lib/ship-policy';
import { BUMP_NEVER_KEY_VERSION, CHANGELOG_FRAGMENTS_KEY_VERSION, DEFAULT_POLICY, POLICY_KEYS, POLICY_KEY_INTRODUCED, REVIEW_SCOPE_KEY_VERSION, STAMP_PLACEHOLDERS_KEY_VERSION, globToRegExp, matchesAny, minimumReader, shipConsequences, validatePolicy } from '../lib/ship-policy';
import { fillDue } from '../lib/ship-queue-train';
import { cmpVersion, parseVersion } from '../lib/version-source';
import { BASE_POLICY, cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
@@ -63,6 +63,32 @@ describe('validatePolicy (schema + containment, pure)', () => {
expect(codes({ preregistration_shas: ['not-a-sha'] })).toEqual(['POLICY_CONTAINMENT:preregistration_shas[0]']);
});
// Value: protects=stamp_placeholders defaults to none, needs its reader floor, and admits only short one-line literals
// that are neither the {version} slot nor a version (GST-1026); fails_when=a non-default value validates without
// min_gstack_version, or a version, a slot, a multi-line or an oversized entry is accepted; seam=validatePolicy
test('stamp_placeholders: empty by default, needs its floor, and admits only short literal placeholders (GST-1026)', () => {
const codes = (p: unknown) => validatePolicy(p).map(e => `${e.code}:${e.path}`);
const at = (stamp_placeholders: unknown) => codes({ stamp_placeholders, min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION });
expect(DEFAULT_POLICY.stamp_placeholders).toEqual([]);
expect(POLICY_KEY_INTRODUCED.stamp_placeholders).toBe(STAMP_PLACEHOLDERS_KEY_VERSION);
expect(codes({ stamp_placeholders: [] })).toEqual([]);
expect(codes({ stamp_placeholders: ['TBD'] })).toEqual(['POLICY_INVALID:stamp_placeholders']);
expect(minimumReader({ stamp_placeholders: ['TBD'] })).toBe(STAMP_PLACEHOLDERS_KEY_VERSION);
expect(at(['TBD', 'UNRELEASED'])).toEqual([]);
expect(at('TBD')).toEqual(['POLICY_INVALID:stamp_placeholders']);
expect(at([7])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
expect(at(Array.from({ length: 17 }, (_, i) => `P${i}`))).toEqual(['POLICY_CONTAINMENT:stamp_placeholders']);
expect(at(['x'.repeat(33)])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
expect(at(['x'.repeat(32)])).toEqual([]);
expect(at(['TB\nD'])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
expect(at([''])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
expect(at([' '])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
expect(at(['{version}'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
expect(at(['next-{version}'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
expect(at(['1.2.3.0'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
expect(at(['0.60.553'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
});
// Value: protects=bump never is refused without its floor (an older gstack names the cause) and next to any policy that stamps a version at every merge, reports its reader floor, and prints its Step 12 consequence; fails_when=never validates bare, or beside stamp-at-merge, train or changelog_fragments;
// why_new=GST-380 added the value to an older key, which the per-key floor check skips; seam=validatePolicy, minimumReader, shipConsequences; no_claim=Step 12's behavior (test/ship-release.test.ts)
test('bump never needs its floor and rules out every policy that stamps at merge', () => {
+38
View File
@@ -53,6 +53,25 @@ describe('E18: a repository without a version source or CHANGELOG', () => {
expect(JSON.parse(fs.readFileSync(path.join(out, 'ship-run.json'), 'utf8')).version).toBe('none');
});
// Value: protects=a receipt write --version named in a repository with no version source keeps that version through a
// carry (GST-1030 review); fails_when=the carry refuses because HEAD has no readable version, or drops the version;
// seam=bin/gstack-ship-receipt cmdCarry
test('a carry keeps the version write --version named when HEAD has no version source (GST-1030)', () => {
const repo = fixture();
for (const f of ['VERSION', 'package.json', 'package-lock.json']) fs.rmSync(path.join(repo.work, f));
repo.git('add', '-A'); repo.git('commit', '-qm', 'changelog only'); repo.git('push', '-q', 'origin', 'main');
makePr(repo, 'pr-unversioned');
const body = path.join(repo.root, 'body.md');
fs.writeFileSync(body, '## Summary\n');
expect(runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3', '--base', 'main', '--session-kind', 'x', '--version', '2.0.0', '--body', body]).status).toBe(EXIT.ok);
fs.mkdirSync(path.join(repo.work, 'test'), { recursive: true });
fs.writeFileSync(path.join(repo.work, 'test', 'a.test.ts'), 'test a\n');
repo.git('add', '-A'); repo.git('commit', '-qm', 'a test-only change');
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(carried.out, carried.err).toContain('CARRY: carried');
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('2.0.0');
});
test('a repository with a version source keeps its version, and one with only a CHANGELOG records no version rather than "none"', () => {
const repo = fixture();
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'x', '--json']);
@@ -248,6 +267,25 @@ describe('carry follow-ups: git floor, named refusal, branch-touched walk, digit
expect(stampOnlyText({ before, after: lines(key('1.2.3.0'), key('1.2.4.0')), oldVersion: '1.2.4.0', newVersion: '1.2.4.0', templates })).toBe(false);
});
// Value: protects=the carry classifier reads a declared placeholder moved to the new version on the branch's own row
// as a stamp, through the same predicate as finish and the tree receipt (GST-1026); fails_when=stampOnlyText ignores
// the policy's placeholders, or accepts an undeclared one or a base row; seam=lib/ship-receipt.ts stampOnlyText
test('stampOnlyText reads a declared placeholder moved on the branch\'s own row as a stamp, and nothing else (GST-1026)', () => {
const templates = ["since: '{version}'"];
const row = (v: string, id: string) => ` { since: '${v}', id: '${id}' },`;
const text = (...rows: string[]) => ['export const ROWS = [', ...rows, '];', ''].join('\n');
const before = text(row('TBD', 'base'), row('TBD', 'mine'));
const side = { fork: '0.60.552.0', added: new Set([row('TBD', 'mine')]), at: new Set([3]) };
const change = (after: string, extra: Record<string, unknown> = {}) => stampOnlyText({ before, after, oldVersion: '0.60.552.0', newVersion: '0.60.553.0', templates, side, placeholders: ['TBD'], ...extra });
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')))).toBe(true);
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { oldVersion: '0.60.553.0' })).toBe(true);
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { placeholders: [] })).toBe(false);
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { placeholders: ['WIP'] })).toBe(false);
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { templates: [] })).toBe(false);
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { side: undefined })).toBe(false);
expect(change(text(row('0.60.553.0', 'base'), row('TBD', 'mine')))).toBe(false);
});
test('planCarry names at most CARRY_NAMED_PATHS paths', () => {
const many = Array.from({ length: CARRY_NAMED_PATHS + 3 }, (_, i) => `src/f${i}.ts`);
const plan = planCarry({ from: 'a'.repeat(40), head: 'b'.repeat(40), changed: many.map(p => ({ path: p, class: 'product' as const })), productPatchId: { from: 'x', head: 'y' }, testPatchId: { from: '', head: '' }, changedProduct: many, mergeCommits: 0, rangeDiff: '' });
+7 -2
View File
@@ -7,8 +7,11 @@ import * as path from 'node:path';
export interface FakeWorld {
runs: Array<Record<string, unknown> & { id: number; run_attempt: number; head_sha: string; path: string }>;
/** Keyed `<run id>/<attempt>` for one attempt's jobs, else `<run id>` for every attempt. */
jobs: Record<string, Array<{ name: string; conclusion: string | null; completed_at?: string }>>;
trees: Record<string, Record<string, unknown>>;
/** Once `marker` exists, the next run read writes `content` to `file` (a working-tree edit inside a run recheck). */
editWhenArmed?: { marker: string; file: string; content: string };
}
/** The stub body for shipFixture's gh(): run this file with the world beside it. */
@@ -27,11 +30,13 @@ if (import.meta.main) {
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); }
const e = world.editWhenArmed;
if (e && fs.existsSync(e.marker)) fs.writeFileSync(e.file, e.content);
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+)\/attempts\/(\d+)\/jobs$/.exec(route!))) {
lines(world.jobs[`${m[1]}/${m[2]}`] ?? 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); }
+317
View File
@@ -0,0 +1,317 @@
/**
* gstack-pregate clear --flaky-exception (GST-1096): a red GitHub Actions run clears a requires-remote obligation only
* when every failure is named from the run's own shard results and logs, each named file is outside the PR's diff and
* its import closure, at most three cases, none paid or a security scan, and an owner's gstack-approvals row binds the
* repository, PR, head, tree, lane, run, attempt and cases. verify prints pass-with-exception, ready prints
* FLAKY_EXCEPTION and still needs green CI (pregate only), and the receipt records it. The run API is a fake; trees
* come from the fixture repository's git.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, type PregateFile } from '../lib/headless-artifacts';
import { buildContext } from '../lib/pregate/context';
import { clearDecision, clearObligation, runPregate, verifyPregate, type ClearDecision, type RunCheck } from '../lib/pregate/index';
import { exceptionBinding, type FailureDeps, type GrantRow } from '../lib/pregate/flaky-exception';
import type { ApiJob, ApiRun, RunDeps, RunProfile } from '../lib/run-evidence';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
import { cleanup as cleanupQueue, makeFixtureRepo as makeQueueRepo, makePr, runBin as runQueueBin } from './helpers/restamp-fixture';
import { ghQueueShim } from './helpers/ship-queue-fixture';
import { shippedBody } from './helpers/ship-queue-train-fixture';
const repos: FixtureRepo[] = [];
const queueRepos: Array<ReturnType<typeof makeQueueRepo>> = [];
afterEach(() => {
while (repos.length) cleanup(repos.pop()!);
while (queueRepos.length) cleanupQueue(queueRepos.pop()!);
});
const LANE = 'pregate.json/remote_gate';
const NOW = new Date('2026-10-29T12:00:00Z');
const PROFILE_NAME = 'ci-reuse/free-tests';
const PROFILE: RunProfile = { workflow: 'free-tests.yml', events: ['pull_request', 'workflow_dispatch'], jobs: ['free-plan', 'typecheck', 'free-tests'], artifact: 'tested-tree' };
const GATE_RUNS = { profiles: [PROFILE_NAME], inventory: 'true' };
const FLAKE = 'test/retry.test.js::retries three times';
const WORDS = 'yes, accept the retry timing flake for this head';
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' }, 'the real change');
return repo;
}
async function pregateAt(repo: FixtureRepo): Promise<PregateFile> {
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(file.requires_remote).toEqual([expect.objectContaining({ lane: LANE, receipt: null })]);
return file;
}
interface Shard { files: string[]; failures?: Array<[string | null, string]>; status?: 'passed' | 'failed' | 'timed-out'; unattributed?: number; retry?: { files: string[]; failures: Array<[string, string]> }; log?: string | null }
interface RunOver { conclusion?: string; jobs?: (jobs: ApiJob[]) => ApiJob[]; missing?: string[]; pull?: { head_sha: string; state: string; head_repo: string | null }; newer?: boolean; attemptAfter?: number; api?: Partial<ApiRun> }
const DEFAULT_SHARDS: Shard[] = [
{ files: ['test/retry.test.js', 'test/a.test.ts'], failures: [['test/retry.test.js', 'retries three times']] },
{ files: ['test/b.test.ts', 'test/util.test.js', 'test/config-read.test.js', 'test/tool-cli.test.js'] },
];
function logFor(s: Shard, names: Array<[string | null, string]>): string {
const lines: string[] = [];
for (const f of s.files) {
lines.push(`${f}:`);
for (const [file, name] of names) if (file === f) lines.push(`(fail) ${name} [5005.00ms]`);
}
lines.push(`Ran ${s.files.length * 3} tests across ${s.files.length} files. [12.00s]`);
return lines.join('\n') + '\n';
}
/** Run 400: a red workflow_dispatch Free Tests run on `head` whose shard artifacts and logs say what failed. */
function redRun(repo: FixtureRepo, head: string, shards: Shard[] = DEFAULT_SHARDS, over: RunOver = {}): RunCheck & { failures: FailureDeps } {
const plan = { version: 1, revision: head, id: 'plan-1', shards: shards.map((s, i) => ({ shard: i + 1, files: s.files, predictedMs: 1000 })) };
const statusOf = (s: Shard) => s.status ?? (s.failures?.length || s.unattributed ? 'failed' : 'passed');
const finalFailed = (s: Shard) => statusOf(s) !== 'passed' && !(s.retry && s.retry.failures.length === 0);
const outcome = (s: Shard, i: number, files: string[], failures: Array<[string | null, string]>, status: string) => ({
shard: i + 1, files, status, exitCode: status === 'passed' ? 0 : 1, elapsedMs: 1000, groupPid: null,
summary: { testsRan: files.length * 3, filesRan: files.length, sawTerminalSummary: status !== 'timed-out' },
failingFiles: [...new Set(failures.map(([f]) => f).filter((f): f is string => !!f))], unattributedFailures: s.unattributed ?? 0,
failures: failures.map(([file, testName]) => ({ file, testName })),
});
const artifacts: Record<string, Record<string, string>> = { 'free-plan': { 'free-plan.json': JSON.stringify(plan) } };
shards.forEach((s, i) => {
const retry = s.retry ? outcome({ files: s.retry.files }, i, s.retry.files, s.retry.failures, s.retry.failures.length ? 'failed' : 'passed') : null;
artifacts[`free-result-${i + 1}`] = { [`shard-${i + 1}.json`]: JSON.stringify({ planId: 'plan-1', revision: head, outcome: outcome(s, i, s.files, s.failures ?? [], statusOf(s)), retry }) };
if (statusOf(s) !== 'passed' && s.log !== null) artifacts[`free-test-shard-logs-${i + 1}`] = { [`gstack-free-test-${i + 1}.log`]: s.log ?? logFor(s, [...(s.failures ?? []), ...(s.retry?.failures ?? [])]) };
});
for (const m of over.missing ?? []) delete artifacts[m];
const anyFailed = shards.some(finalFailed);
const at = '2026-10-29T10:30:00Z';
const baseJobs: ApiJob[] = [
{ name: 'reuse', conclusion: 'skipped', completed_at: at },
{ name: 'free-plan', conclusion: 'success', completed_at: at },
{ name: 'typecheck', conclusion: 'success', completed_at: at },
...shards.map((s, i) => ({ name: `free-suite (${i + 1})`, conclusion: finalFailed(s) ? 'failure' : 'success', completed_at: at })),
{ name: 'free-tests', conclusion: anyFailed ? 'failure' : 'success', completed_at: at },
];
const jobs = over.jobs ? over.jobs(baseJobs) : baseJobs;
let reads = 0;
const api = (): ApiRun => ({
id: 400, run_attempt: reads++ > 0 && over.attemptAfter ? over.attemptAfter : 1, status: 'completed', conclusion: over.conclusion ?? (anyFailed ? 'failure' : 'success'), event: 'workflow_dispatch', head_sha: head,
path: '.github/workflows/free-tests.yml', created_at: '2026-10-29T10:00:00Z', updated_at: at, repository: { full_name: 'fx/fx' }, head_repository: { full_name: 'fx/fx' }, ...over.api,
});
const deps: RunDeps = {
run: id => { if (id !== 400) throw new Error('HTTP 404'); return api(); },
runsForHead: () => [{ ...api(), run_attempt: 1 }, ...(over.newer ? [{ ...api(), id: 401, run_attempt: 1, created_at: '2026-10-29T11:00:00Z', run_started_at: '2026-10-29T11:00:00Z' }] : [])],
jobs: () => jobs,
artifact: () => { throw new Error('run 400 has 0 unexpired artifacts named tested-tree'); },
commitTree: sha => repo.git('rev-parse', `${sha}^{tree}`),
};
const failures: FailureDeps = {
artifactFiles: (_id, name) => { const a = artifacts[name]; if (!a) throw new Error(`run 400 has 0 unexpired artifacts named ${name}`); return a; },
pull: () => over.pull ?? { head_sha: head, state: 'open', head_repo: 'fx/fx' },
};
return { repo: 'fx/fx', deps, profiles: { [PROFILE_NAME]: PROFILE }, gateRuns: GATE_RUNS, now: NOW, inventory: () => ['test/a.test.ts', 'test/b.test.ts'], failures };
}
const msg = (d: ClearDecision) => ('refused' in d ? `${d.refused}: ${d.message}${d.fix ? `; fix: ${d.fix}` : ''}` : `cleared ${d.receipt}`);
/** The scope the grant-needed refusal names, as an approvals row the owner granted. */
function grantFor(d: ClearDecision, over: Partial<GrantRow> = {}): (id: string) => GrantRow | null {
const scope = /--scope "(flaky-exception:[0-9a-f]{64})"/.exec(msg(d))?.[1];
if (!scope) throw new Error(`no grant scope in: ${msg(d)}`);
return id => (id === 'a-1' ? { id, outcome: 'approved', scope, words: WORDS, by: 'Garry', ...over } : null);
}
describe.skipIf(process.platform === 'win32')('clear --flaky-exception: an owner-granted exception for named flaky cases', () => {
test('a red run whose only failure is the named case clears the gate once the bound grant exists; verify reports pass-with-exception', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const check = redRun(repo, head);
const asked = clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7 } }, repo.work);
expect(msg(asked)).toMatch(/^PREGATE_RECEIPT_INVALID: FLAKY_EXCEPTION: refused reason=grant_needed — .*test\/retry\.test\.js::retries three times.*; fix: .*gstack-approvals add --ask '.*' --words "<the owner's exact words>" --by "<owner>" --scope "flaky-exception:[0-9a-f]{64}" --source pregate-flaky-exception/);
const d = clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7, grant: 'a-1', grantRow: grantFor(asked) } }, repo.work);
if ('refused' in d) throw new Error(d.message);
expect(d.receipt).toBe(`remote-gate tree=${file.tree.slice(0, 12)} tests=18 exception=a-1 cases=1 runs=400/1 profiles=${PROFILE_NAME} heads=${head.slice(0, 12)} verified=2026-10-29T12:00:00.000Z`);
expect(d.exception).toMatchObject({ lane: LANE, repo: 'fx/fx', pr: 7, head, tree: file.tree, runs: ['400/1'], cases: [FLAKE], grant: 'a-1', by: 'Garry', words: WORDS });
const cleared = clearObligation(file, LANE, d.receipt, 'test', d.substitution, d.exception);
expect(cleared.verdict).toBe('pass-with-exception');
expect(cleared.exceptions).toEqual([d.exception!]);
const v = verifyPregate(repo.work, cleared, { tree: file.tree } as any);
expect(v).toMatchObject({ ok: true });
expect(v.message).toContain(`flaky exception ${FLAKE} (grant a-1, by Garry, run 400/1)`);
const tampered = (e: Partial<NonNullable<PregateFile['exceptions']>[number]>) => verifyPregate(repo.work, { ...cleared, exceptions: [{ ...cleared.exceptions![0]!, ...e }] }, { tree: file.tree } as any);
expect(tampered({ head: 'f'.repeat(40) })).toMatchObject({ ok: false, code: 'PREGATE_REMOTE_UNCLEARED' });
expect(tampered({ cases: [FLAKE, 'test/b.test.ts::other'] }).message).toContain('binding does not match');
expect(verifyPregate(repo.work, { ...cleared, exceptions: [] }, { tree: file.tree } as any).message).toContain('no exceptions entry');
const plain = clearObligation(structuredClone(cleared), LANE, `remote-gate tree=${file.tree.slice(0, 12)} tests=18 runs=401/1 profiles=${PROFILE_NAME} heads=${head.slice(0, 12)} verified=2026-10-29T12:00:00.000Z`, 'test');
expect([plain.exceptions, plain.verdict]).toEqual([undefined, 'requires-remote cleared']);
}, 120_000);
test('the grant binds the head, PR, run and cases: another scope, an unapproved row, an agent\'s words or an unknown id refuse', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const check = redRun(repo, head);
const asked = clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7 } }, repo.work);
const run = (grantRow: (id: string) => GrantRow | null, grant = 'a-1') => msg(clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7, grant, grantRow } }, repo.work));
const otherHead = exceptionBinding({ lane: LANE, repo: 'fx/fx', pr: 7, head: 'e'.repeat(40), tree: file.tree, runs: ['400/1'], cases: [FLAKE] });
expect(run(grantFor(asked, { scope: otherHead }))).toContain('reason=grant_mismatch');
expect(run(grantFor(asked, { outcome: 'pending' }))).toContain('reason=grant_not_approved');
expect(run(grantFor(asked, { by: 'agent:jam_123' }))).toContain('reason=grant_not_owner');
expect(run(grantFor(asked), 'a-2')).toContain('reason=grant_missing');
}, 120_000);
test('every failure must be named: an extra failure, a case that did not fail, more than three cases, an unattributed failure refuse', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const clear = (check: RunCheck, cases = [FLAKE]) => msg(clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases, pr: 7 } }, repo.work));
const two: Shard[] = [{ files: ['test/retry.test.js', 'test/a.test.ts'], failures: [['test/retry.test.js', 'retries three times'], ['test/a.test.ts', 'other']] }, DEFAULT_SHARDS[1]!];
expect(clear(redRun(repo, head, two))).toContain('reason=unnamed_failure — run 400 attempt 1 failed test/a.test.ts::other, which no --flaky-exception names');
expect(clear(redRun(repo, head), [FLAKE, 'test/a.test.ts::passed fine'])).toContain('reason=case_not_failed — test/a.test.ts::passed fine did not fail in run 400');
expect(clear(redRun(repo, head), ['a.ts::1', 'b.ts::2', 'c.ts::3', 'd.ts::4'])).toBe('usage: --flaky-exception names at most 3 cases; got 4');
expect(clear(redRun(repo, head), ['no-separator'])).toContain('usage: --flaky-exception takes "<test file>::<case>"');
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, unattributed: 1 }, DEFAULT_SHARDS[1]!]))).toContain('reason=unnamed_failure — shard 1: 1 failure(s) the runner could not attribute to a test');
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, failures: [[null, 'retries three times']] }, DEFAULT_SHARDS[1]!]))).toContain('reason=unnamed_failure');
expect(clear(redRun(repo, head, [{ files: DEFAULT_SHARDS[0]!.files }, DEFAULT_SHARDS[1]!]))).toContain('reason=run_green');
}, 120_000);
test('every job, shard and log is reconciled: a failed non-shard job, a missing result or log, a timed-out shard, a log failure the result omits refuse', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const clear = (check: RunCheck) => msg(clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7 } }, repo.work));
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { jobs: js => js.map(j => j.name === 'typecheck' ? { ...j, conclusion: 'failure' } : j) }))).toContain('reason=unnamed_failure — run 400 attempt 1\'s typecheck job concluded failure; only test-shard failures can be named');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { jobs: js => js.map(j => j.name === 'reuse' ? { ...j, conclusion: 'cancelled' } : j) }))).toContain('reason=unnamed_failure — run 400 attempt 1\'s reuse job concluded cancelled');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { jobs: js => js.filter(j => j.name !== 'free-suite (2)') }))).toContain('reason=shard_mismatch');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { missing: ['free-result-2'] }))).toContain('reason=shard_incomplete — shard 2 has no readable free-result-2');
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, log: null }, DEFAULT_SHARDS[1]!]))).toContain('reason=logs_missing — shard 1 failed and has no readable free-test-shard-logs-1');
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, status: 'timed-out' }, DEFAULT_SHARDS[1]!]))).toContain('reason=shard_incomplete — shard 1 timed out');
const extraInLog = logFor(DEFAULT_SHARDS[0]!, [['test/retry.test.js', 'retries three times'], ['test/a.test.ts', 'hidden']]);
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, log: extraInLog }, DEFAULT_SHARDS[1]!]))).toContain('reason=unnamed_failure — shard 1\'s log names test/a.test.ts::hidden, which its result does not');
expect(clear(redRun(repo, head, [{ ...DEFAULT_SHARDS[0]!, log: logFor(DEFAULT_SHARDS[0]!, [['test/retry.test.js', 'retries three times']]) + '# Unhandled error between tests\n' }, DEFAULT_SHARDS[1]!]))).toContain('reason=unnamed_failure — shard 1\'s log shows 1 unhandled error(s) between tests');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { conclusion: 'cancelled' }))).toContain('reason=run_not_failure');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { newer: true }))).toContain('reason=newer_run');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { attemptAfter: 2 }))).toContain('reason=run_changed');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { api: { repository: { full_name: 'other/fx' } } }))).toContain('reason=other_repository');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { api: { head_repository: { full_name: 'fork/fx' } } }))).toContain('reason=fork_run');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { api: { path: '.github/workflows/evals.yml' } }))).toContain('reason=workflow_mismatch');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { api: { event: 'push' } }))).toContain('reason=event_mismatch');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { api: { status: 'in_progress' } }))).toContain('reason=run_in_progress');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { jobs: js => js.map(j => ({ ...j, completed_at: '2026-10-27T10:30:00Z' })) }))).toContain('reason=run_stale');
}, 120_000);
test('a red run that moves while the clear is assembled refuses, and the printed grant command quotes case names for the shell', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const check = redRun(repo, head);
const asked = clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7 } }, repo.work);
let rerun = false;
const run = check.deps.run;
const moving: RunCheck = { ...check, deps: { ...check.deps, run: id => (rerun ? { ...run(id), run_attempt: 2, status: 'in_progress' } : run(id)) }, inventory: () => { rerun = true; return ['test/a.test.ts', 'test/b.test.ts'] } };
expect(msg(clearDecision(file, LANE, { runs: ['400'], evidence: moving, flaky: { cases: [FLAKE], pr: 7, grant: 'a-1', grantRow: grantFor(asked) } }, repo.work))).toContain('reason=run_changed — run 400 moved to attempt 2 (in_progress)');
const name = "times out $(touch pwned) `id` it's";
const shard: Shard[] = [{ files: ['test/retry.test.js', 'test/a.test.ts'], failures: [['test/retry.test.js', name]] }, DEFAULT_SHARDS[1]!];
const fix = msg(clearDecision(file, LANE, { runs: ['400'], evidence: redRun(repo, head, shard), flaky: { cases: [`test/retry.test.js::${name}`], pr: 7 } }, repo.work));
const ask = /--ask ('(?:[^']|'\\'')*') --words/.exec(fix)?.[1];
expect(ask).toBeDefined();
const echoed = Bun.spawnSync(['bash', '-c', `printf %s ${ask}`], { cwd: repo.work, timeout: 10_000 });
expect(echoed.stdout.toString()).toContain(`test/retry.test.js::${name}`);
expect(fs.existsSync(path.join(repo.work, 'pwned'))).toBe(false);
}, 120_000);
test('a retry that passed is a recovered flake; a retry that failed again keeps only its own failures', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const shard1: Shard = { files: ['test/retry.test.js', 'test/a.test.ts'], failures: [['test/retry.test.js', 'retries three times'], ['test/a.test.ts', 'recovered']], retry: { files: ['test/retry.test.js', 'test/a.test.ts'], failures: [['test/retry.test.js', 'retries three times']] } };
expect(msg(clearDecision(file, LANE, { runs: ['400'], evidence: redRun(repo, head, [shard1, DEFAULT_SHARDS[1]!]), flaky: { cases: [FLAKE], pr: 7 } }, repo.work))).toContain('reason=grant_needed');
}, 120_000);
test('a named file inside the PR diff or its import closure, a paid file or a security scan test refuses', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const one = (fileName: string, name: string) => {
const check = redRun(repo, head, [{ files: [fileName, 'test/retry.test.js', 'test/a.test.ts'].filter((f, i, a) => a.indexOf(f) === i), failures: [[fileName, name]] }, { files: DEFAULT_SHARDS[1]!.files.filter(f => f !== fileName) }]);
return msg(clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [`${fileName}::${name}`], pr: 7 } }, repo.work));
};
expect(one('test/util.test.js', 'adds')).toContain('reason=case_in_closure — test/util.test.js depends on a file the PR changes (lib/util.js)');
expect(one('test/skill-e2e-flaky.test.ts', 'judge')).toContain('reason=case_paid — test/skill-e2e-flaky.test.ts is a paid test file');
expect(one('test/cso-docker-policy.test.ts', 'times out')).toContain('reason=case_security — test/cso-docker-policy.test.ts tests a security scan');
expect(one('test/ship-template-redaction.test.ts', 'blocks')).toContain('reason=case_security');
expect(one('browse/test/content-security.test.ts', 'blocks')).toContain('reason=case_security');
const touched = fixture();
commit(touched, { 'test/retry.test.js': "const test = require('node:test');\ntest('retries three times', () => {});\n" }, 'touch the flaky test');
const touchedHead = touched.git('rev-parse', 'HEAD');
const touchedFile = await pregateAt(touched);
expect(msg(clearDecision(touchedFile, LANE, { runs: ['400'], evidence: redRun(touched, touchedHead), flaky: { cases: [FLAKE], pr: 7 } }, touched.work))).toContain('reason=case_in_diff — test/retry.test.js is in the PR\'s diff');
}, 120_000);
test('the run must be at pregate.json\'s own head and tree, and the PR head must be that head: no rebinding', async () => {
const repo = fixture();
const older = repo.git('rev-parse', 'HEAD~1');
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const clear = (check: RunCheck) => msg(clearDecision(file, LANE, { runs: ['400'], evidence: check, flaky: { cases: [FLAKE], pr: 7 } }, repo.work));
expect(clear(redRun(repo, older))).toContain('reason=head_mismatch');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { pull: { head_sha: older, state: 'open', head_repo: 'fx/fx' } }))).toContain('reason=pr_mismatch — PR #7\'s head is');
expect(clear(redRun(repo, head, DEFAULT_SHARDS, { pull: { head_sha: head, state: 'closed', head_repo: 'fx/fx' } }))).toContain('reason=pr_mismatch — PR #7 is closed');
}, 120_000);
test('the bins: clear refuses --flaky-exception without --run or --pr; verify prints the PREGATE pass-with-exception line; the receipt records it', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const file = await pregateAt(repo);
const out = path.join(repo.work, '.gstack/tmp/pregate.json');
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, JSON.stringify(file));
const noRun = runBin(repo, 'gstack-pregate', ['clear', '--remote', LANE, '--flaky-exception', FLAKE, '--pr', '7', '--out', out]);
expect([noRun.status, noRun.err]).toEqual([EXIT.usage, expect.stringContaining('--flaky-exception needs --run <id|url>')]);
const noPr = runBin(repo, 'gstack-pregate', ['clear', '--remote', LANE, '--run', '400', '--flaky-exception', FLAKE, '--out', out]);
expect([noPr.status, noPr.err]).toEqual([EXIT.usage, expect.stringContaining('--flaky-exception needs --pr <n>')]);
const exception = { lane: LANE, repo: 'fx/fx', pr: 7, head, tree: file.tree, runs: ['400/1'], cases: [FLAKE] };
const recorded = { ...exception, grant: 'a-1', by: 'Garry', words: WORDS, binding: exceptionBinding(exception) };
const cleared = clearObligation(file, LANE, `remote-gate tree=${file.tree.slice(0, 12)} tests=18 exception=a-1 cases=1 runs=400/1 profiles=${PROFILE_NAME} heads=${head.slice(0, 12)} verified=2026-10-29T12:00:00.000Z`, 'test', undefined, recorded);
fs.writeFileSync(out, JSON.stringify(cleared));
const v = runBin(repo, 'gstack-pregate', ['verify', '--out', out]);
expect([v.status, v.err]).toEqual([EXIT.ok, '']);
expect(v.out).toContain(`PREGATE: pass-with-exception (${FLAKE}; grant a-1)\n`);
const bound = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'interactive', '--pregate', out, '--json']);
expect([bound.status, bound.err]).toEqual([EXIT.ok, expect.any(String)]);
expect(bound.out).toContain('"flaky_exception":[{"lane":"pregate.json/remote_gate","runs":["400/1"],"cases":["test/retry.test.js::retries three times"],"grant":"a-1","by":"Garry"}]');
}, 180_000);
});
describe.skipIf(process.platform === 'win32')('ready with a flaky exception', () => {
test('ready accepts pass-with-exception, prints FLAKY_EXCEPTION, and still refuses a red required check (pregate only)', () => {
const repo = makeQueueRepo();
queueRepos.push(repo);
const head = makePr(repo, 'pr-a');
const tree = runQueueBin(repo, 'gstack-wtree', []).out.trim();
const exception = { lane: LANE, repo: 'o/r', pr: 1, head, tree, runs: ['400/1'], cases: [FLAKE] };
const pregate = path.join(repo.root, 'pregate.json');
fs.writeFileSync(pregate, JSON.stringify({
schema_version: 1, tree, head, verdict: 'pass-with-exception', checks: [{ id: 'lanes', status: 'requires-remote' }],
requires_remote: [{ lane: LANE, platform: 'remote', workflow: 'pregate.json', job: 'remote_gate', tests: ['test/a.test.ts'], receipt: `remote-gate tree=${tree.slice(0, 12)} tests=18 exception=a-1 cases=1 runs=400/1 profiles=${PROFILE_NAME} heads=${head.slice(0, 12)} verified=2026-10-29T12:00:00.000Z` }],
exceptions: [{ ...exception, grant: 'a-1', by: 'Garry', words: WORDS, binding: exceptionBinding(exception) }],
}));
const queue = (checks: unknown) => runQueueBin(repo, 'gstack-ship-queue', ['ready', '--pr', '1', '--pregate', pregate, '--base', 'main'], ghQueueShim(repo, { prs: { '1': { headRefOid: head, url: 'https://github.com/o/r/pull/1', body: shippedBody('1', head) } }, checks: { [head]: checks } }));
const ok = queue({ check_runs: [{ status: 'completed', conclusion: 'success' }] });
expect([ok.status, ok.err]).toEqual([EXIT.ok, '']);
expect(ok.out).toMatch(/^PREGATE: pass-with-exception tree=[0-9a-f]{12} /m);
expect(ok.out).toContain(`FLAKY_EXCEPTION: ${LANE} cases=${FLAKE} run=400/1 grant=a-1 by=Garry (pregate only; every required CI check still needs its own green run at this head)\n`);
const red = queue({ check_runs: [{ status: 'completed', conclusion: 'failure' }] });
expect(red.status).toBe(EXIT.refused);
expect(red.err).toMatch(/CI is not green at .*\(QUEUE_NOT_READY\)/);
const other = { ...exception, pr: 2 };
fs.writeFileSync(pregate, JSON.stringify({ ...JSON.parse(fs.readFileSync(pregate, 'utf8')), exceptions: [{ ...other, grant: 'a-1', by: 'Garry', words: WORDS, binding: exceptionBinding(other) }] }));
const moved = queue({ check_runs: [{ status: 'completed', conclusion: 'success' }] });
expect(moved.status).toBe(EXIT.refused);
expect(moved.err).toContain('flaky exception grant a-1 is bound to PR #2');
}, 120_000);
});
+1 -1
View File
@@ -84,7 +84,7 @@ test('report-dir names the filesystem cause when .gstack is not a directory', ()
fs.writeFileSync(path.join(f.root, '.gstack'), 'not a directory\n');
const r = f.tool('gstack-qa-evidence', 'report-dir');
expect(r.status).toBe(2);
expect(r.stderr).toContain('report-dir failed: ENOTDIR');
expect(r.stderr).toMatch(process.platform === 'win32' ? /report-dir failed: (ENOTDIR|ENOENT)/ : /report-dir failed: ENOTDIR/);
});
test('report-dir refuses outside a git work tree instead of guessing a location', () => {
+158 -1
View File
@@ -14,6 +14,8 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { rewriteStamps, tentativeMentions } from '../lib/restamp';
import { STAMP_PLACEHOLDERS_KEY_VERSION } from '../lib/ship-policy';
import { extractReceipt } from '../lib/ship-receipt';
import type { GitFixtureTemplate } from './helpers/git-fixture-template';
import { BASE_POLICY, cleanup, cloneFixture, fixtureTemplate, makePr, mergeMain, mergeToMain, readFile, restamp, runBin, type FixtureRepo } from './helpers/restamp-fixture';
@@ -425,10 +427,165 @@ describe('a branch-added row left at the fork version after the base moved (GST-
const { repo, body } = carryAfterStamp('restamp-carry-mode-', [row('1.2.3.0', 'base_row')], row('1.2.3.0', 'mine'),
[row('1.2.3.0', 'base_row'), '', row('1.2.5.0', 'mine')]);
fs.chmodSync(path.join(repo.work, NOTICE), 0o755);
repo.git('commit', '-qam', 'stamp 1.2.5.0, and make the file executable');
repo.git('add', '-A');
repo.git('update-index', '--chmod=+x', '--', NOTICE);
repo.git('commit', '-qm', 'stamp 1.2.5.0, and make the file executable');
expect(repo.git('ls-tree', 'HEAD', '--', NOTICE)).toStartWith('100755 ');
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(carried.status, carried.out).toBe(EXIT.refused);
expect(carried.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
});
});
describe('declared placeholders and the receipt carry (GST-1026, GST-1030)', () => {
/** gbrain's policy (GST-1026): the since: template, and TBD declared as a placeholder when `declared`. */
const placeholderPolicy = (declared: boolean) => ({ ...receiptPolicy(true), ...(declared ? { stamp_placeholders: ['TBD'], min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION } : {}) });
/** A branch row written as since: 'TBD' at the claim 1.2.4.0, then the stamp to 1.2.5.0 with the row restamped by hand (gbrain's release:restamp). */
function tbdStamp(prefix: string, pol: Record<string, unknown>, ownRow: string): { repo: FixtureRepo; gated: string } {
const repo = makeFixtureRepo({ prefix, policy: pol }); repos.push(repo);
write(repo, NOTICE, notice([row('TBD', 'base_row')]));
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows'); repo.git('push', '-q', 'origin', 'main');
makePr(repo, 'pr-tbd');
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp 1.2.4.0');
write(repo, NOTICE, notice([row('TBD', 'base_row'), '', ownRow]));
repo.git('commit', '-qam', 'mine: a TBD row');
const gated = repo.git('rev-parse', 'HEAD');
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
return { repo, gated };
}
// Value: protects=the tree receipt (stamp, verify, gate-ahead replay) accepts gbrain's restamp of the branch's own
// since: 'TBD' row when the policy declares TBD, and refuses it undeclared, on a base row, or on a base row whose text
// the branch duplicates (GST-1026); fails_when=classifyFile reads the TBD move as a hand edit, or accepts it off the
// branch's own rows; seam=lib/tree-receipt.ts stampOnlyHunks
test('the tree receipt accepts a declared TBD row restamped on the branch, and refuses the same move on a base row (GST-1026)', () => {
const own = tbdStamp('restamp-tbd-receipt-', placeholderPolicy(true), row('TBD', 'mine'));
write(own.repo, NOTICE, readFile(own.repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
own.repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
const accepted = runBin(own.repo, 'gstack-tree-receipt', ['--gated', own.gated, '--base', 'main']);
expect(accepted.out, accepted.err).toContain('TREE: same-modulo-stamps');
write(own.repo, NOTICE, readFile(own.repo, NOTICE).replace(row('TBD', 'base_row'), row('1.2.5.0', 'base_row')));
own.repo.git('commit', '-qam', 'move the base TBD row by hand');
const base = runBin(own.repo, 'gstack-tree-receipt', ['--gated', own.gated, '--base', 'main']);
expect(base.out, base.err).toContain('TREE: changed');
expect(base.out).toContain("id: 'base_row'");
});
test('the tree receipt refuses an undeclared TBD row restamped on the branch (GST-1026)', () => {
const undeclared = tbdStamp('restamp-tbd-undeclared-', placeholderPolicy(false), row('TBD', 'mine'));
write(undeclared.repo, NOTICE, readFile(undeclared.repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
undeclared.repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
const refused = runBin(undeclared.repo, 'gstack-tree-receipt', ['--gated', undeclared.gated, '--base', 'main']);
expect(refused.out, refused.err).toContain('TREE: changed');
});
test('the tree receipt refuses a restamp of the base copy of a TBD row the branch duplicates (GST-1026)', () => {
const dup = tbdStamp('restamp-tbd-dup-', placeholderPolicy(true), row('TBD', 'base_row'));
write(dup.repo, NOTICE, notice([row('1.2.5.0', 'base_row'), '', row('TBD', 'base_row')]));
dup.repo.git('commit', '-qam', 'move the base copy of the duplicated TBD row');
const copy = runBin(dup.repo, 'gstack-tree-receipt', ['--gated', dup.gated, '--base', 'main']);
expect(copy.out, copy.err).toContain('TREE: changed');
});
/** A receipt written at the claim 1.2.4.0 after `own` changed the branch, then the stamp to 1.2.5.0, uncommitted. */
function receiptThenStamp(prefix: string, pol: Record<string, unknown>, own: (repo: FixtureRepo) => void): { repo: FixtureRepo; body: string } {
const repo = makeFixtureRepo({ prefix, policy: pol }); repos.push(repo);
write(repo, NOTICE, notice([row('TBD', 'base_row')]));
fs.rmSync(path.join(repo.work, 'package-lock.json'));
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows, no lockfile'); repo.git('push', '-q', 'origin', 'main');
makePr(repo, 'pr-carry');
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp 1.2.4.0');
own(repo);
repo.git('add', '-A'); repo.git('commit', '-qm', 'mine');
const body = path.join(repo.root, 'body.md');
fs.writeFileSync(body, '## Summary\n');
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3', '--base', 'main', '--session-kind', 'x', '--body', body]);
expect(w.status, w.err).toBe(EXIT.ok);
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
return { repo, body };
}
const carry = (repo: FixtureRepo, body: string) => runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
// Value: protects=the receipt carry follows gbrain's restamp of the branch's own since: 'TBD' row when the policy
// declares TBD, and refuses it undeclared (GST-1026); fails_when=productSpec's own-row check ignores the policy's
// placeholders and refuses RECEIPT_CARRY_PRODUCT_CHANGED; seam=bin/gstack-ship-receipt productSpec ownRows
test.each([['declared', true], ['undeclared', false]])('the carry and a TBD row restamped on the branch: %s', (_, declared) => {
const { repo, body } = receiptThenStamp(`restamp-tbd-carry-${declared}-`, placeholderPolicy(declared), r => write(r, NOTICE, notice([row('TBD', 'base_row'), '', row('TBD', 'mine')])));
write(repo, NOTICE, readFile(repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
const carried = carry(repo, body);
if (declared) expect(carried.out, carried.err).toContain('CARRY: carried');
else expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
});
// Value: protects=a carried receipt names the head's version, the one write would record there, after a main merge
// and a restamp (GST-1030 point 1, GGST-1); fails_when=carryReceipt copies version from the old receipt;
// seam=bin/gstack-ship-receipt cmdCarry
test('the carried receipt records the head\'s version after a main merge and a restamp (GST-1030)', () => {
const { repo, body } = receiptThenStamp('restamp-carry-version-', policy(true), r => write(r, 'src.ts', 'export const x = 0;\n'));
repo.git('reset', '-q', '--hard');
expect(readFile(repo, 'VERSION')).toBe('1.2.4.0\n');
makePr(repo, 'pr-other');
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp other');
mergeToMain(repo, 'pr-other');
repo.git('checkout', '-q', 'pr-carry');
mergeMain(repo);
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'restamp after the lost slot');
expect(readFile(repo, 'VERSION')).toBe('1.2.5.0\n');
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('1.2.4.0');
const carried = carry(repo, body);
expect(carried.out, carried.err).toContain('CARRY: carried');
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('1.2.5.0');
});
/** package.json with its dependency pin set to `pin`, every other field kept. */
const pinned = (repo: FixtureRepo, pin: string) => {
const pkg = JSON.parse(readFile(repo, 'package.json'));
pkg.dependencies = { ...pkg.dependencies, 'left-pad': pin };
write(repo, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
};
// Value: protects=a pin-bump PR's receipt carries across a main merge and a restamp when package.json's only change since
// review is its version field, and refuses when the pin itself moved (GST-1030, GBEV-3, gbrain-evals #306);
// fails_when=the carry counts the PR's package.json as product and refuses RECEIPT_CARRY_PRODUCT_CHANGED on a version-only
// move, or lets a changed dependency pin ride; seam=bin/gstack-ship-receipt cmdCarry walk
test.each([['the pin unchanged since review', false], ['the pin moved since review', true]])('the carry and a pin-bump PR after a main merge: %s (GST-1030)', (_, moved) => {
const { repo, body } = receiptThenStamp(`restamp-carry-pin-${moved}-`, policy(true), r => pinned(r, '1.3.0'));
repo.git('reset', '-q', '--hard');
makePr(repo, 'pr-other');
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp other');
mergeToMain(repo, 'pr-other');
repo.git('checkout', '-q', 'pr-carry');
mergeMain(repo);
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
if (moved) pinned(repo, '1.3.1');
repo.git('commit', '-qam', 'restamp after the lost slot');
expect(JSON.parse(readFile(repo, 'package.json')).version).toBe('1.2.5');
const carried = carry(repo, body);
if (moved) expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
else expect(carried.out, carried.err).toContain('CARRY: carried');
});
// Value: protects=a version-only restamp of EVIDENCE.md is release metadata to the carry when the policy declares it in
// stamp_files, and product when it does not (GST-1030 point 2, GGST-1); fails_when=the carry counts the declared
// file's stamp as a product change (RECEIPT_CARRY_PRODUCT_CHANGED), or lets an undeclared file's version edit ride;
// seam=bin/gstack-ship-receipt productSpec, lib/ship-receipt.ts stampOnlyText
test.each([['declared in stamp_files', true], ['undeclared', false]])('the carry and a version-only restamp of EVIDENCE.md: %s (GST-1030)', (_, declared) => {
const base = policy(true);
const pol = declared ? { ...base, stamp_files: [...base.stamp_files, 'EVIDENCE.md'] } : base;
const { repo, body } = receiptThenStamp(`restamp-carry-evidence-${declared}-`, pol, r => write(r, 'EVIDENCE.md', '# Evidence\n\nVerified on v1.2.4.0 with the free suite\n'));
if (!declared) write(repo, 'EVIDENCE.md', '# Evidence\n\nVerified on v1.2.5.0 with the free suite\n');
expect(readFile(repo, 'EVIDENCE.md')).toBe('# Evidence\n\nVerified on v1.2.5.0 with the free suite\n');
repo.git('commit', '-qam', 'stamp 1.2.5.0');
const carried = carry(repo, body);
if (declared) expect(carried.out, carried.err).toContain('CARRY: carried');
else expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
});
});
+104 -1
View File
@@ -11,7 +11,7 @@ import { EXIT } from '../lib/headless-artifacts';
import { KEEP_ARG_MAX_BYTES, readSelection, reviewExclude, tooLargeLine, unreadableLine } from '../lib/review-exclude';
import { collectCommit, collectLive } from '../lib/review-collect';
import { extractReceipt } from '../lib/ship-receipt';
import { bindShippedReceipt, receiptReviewAtHead } from '../lib/ship-receipt-depth';
import { bindShippedReceipt, excludeMismatch, receiptReviewAtHead } from '../lib/ship-receipt-depth';
import { REVIEW_EXCLUDE_KEY_VERSION, isUnavailable } from '../lib/ship-policy';
import { loadPolicyAt } from '../lib/ship-policy-at';
import { cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
@@ -313,6 +313,31 @@ describe('the receipt binds the selection the reviewers read', () => {
expect(removed.status).toBe(EXIT.fail);
expect(removed.err).toContain(`field=review_exclude expected=none recorded=${digest}`);
}, 240_000);
// Value: protects=every place that describes write --review-exclude says what it checks: the digest HEAD has under the base
// policy, not that a review read that selection; fails_when=the help, troubleshooting, ship-policy or the queue's refusal
// line claims a receipt write alone cannot clear field=review_exclude while a write with the refusal's own expected= digest
// clears it with no review run; why_new=GST-972 item 2 (the #3852 cross-family trail review); seam=gstack-ship-receipt write
// and --help, excludeMismatch, the two docs; no_claim=binding the digest to the review ledger (write never reads it)
test('write binds --review-exclude to the base policy, not to a review, and no description claims more', () => {
const repo = census();
const body = path.join(repo.root, 'body.md');
const refused = writeReceipt(repo, body, []);
const expected = refused.err.match(/field=review_exclude expected=([0-9a-f]{64}) /)?.[1];
expect(expected).toBeDefined();
expect(writeReceipt(repo, body, ['--review-exclude', expected!, ...targetedAtHead(repo)]).status).toBe(EXIT.ok);
const doc = (rel: string) => fs.readFileSync(path.join(import.meta.dir, '..', rel), 'utf8').replace(/\s+/g, ' ');
const claims = {
help: runBin(repo, 'gstack-ship-receipt', ['--help']).out.replace(/\s+/g, ' '),
troubleshooting: doc('docs/troubleshooting.md'),
shipPolicy: doc('docs/ship-policy.md'),
queue: excludeMismatch({ state: 'none' }, { review_exclude: { digest: expected } } as any, repo.git('rev-parse', 'HEAD'), 'origin/main')!,
};
for (const [where, text] of Object.entries(claims)) {
expect({ where, overClaim: /write alone (cannot|does not) clear|never a receipt write alone/i.test(text) }).toEqual({ where, overClaim: false });
expect({ where, says: text.includes('does not read the review ledger') }).toEqual({ where, says: true });
}
}, 240_000);
});
describe('a branch cannot hide code under a base pattern', () => {
@@ -348,6 +373,84 @@ describe('a branch cannot hide code under a base pattern', () => {
expect(p).toMatchObject({ depth: 'full', errors: expect.arrayContaining([expect.objectContaining({ code: 'binary_code_path', input: 'assets/loader.js' })]) });
}, 120_000);
// Value: protects=a compiled binary under a base pattern stays in review, because it is code; fails_when=a .node, .so, .so.N,
// .dylib or .dll file, or an ELF, Mach-O or PE image under a content extension (.bin, .dat), is excluded as binary content;
// why_new=GST-972 item 3; seam=bin/gstack-review-plan; no_claim=what a reader can see inside a binary
test('native binaries are code: .node, .so, .dylib, .dll and executable images under a content extension stay kept', () => {
const repo = makeFixtureRepo({ prefix: 'gstack-exclude-', baseFiles: { '.gstack/ship-policy.json': policyText(['assets/**']) } }); repos.push(repo);
fs.mkdirSync(path.join(repo.work, 'assets'), { recursive: true });
const elf = Buffer.concat([Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]), Buffer.alloc(24)]);
const macho = Buffer.concat([Buffer.from([0xcf, 0xfa, 0xed, 0xfe]), Buffer.alloc(28)]);
const pe = Buffer.concat([Buffer.from('MZ'), Buffer.alloc(30)]);
const files = { 'assets/addon.node': elf, 'assets/libx.dylib': macho, 'assets/libx.so': elf, 'assets/libx.so.1': elf, 'assets/setup.dat': pe, 'assets/tool.bin': elf, 'assets/x.dll': pe, 'assets/real.gz': GZ };
for (const [rel, bytes] of Object.entries(files)) fs.writeFileSync(path.join(repo.work, rel), bytes);
commit(repo, {});
const p = JSON.parse(plan(repo, ['--json']).out);
expect(p.review_exclude.kept_files).toEqual(Object.keys(files).filter(f => f !== 'assets/real.gz').map(f => ({ path: f, reason: 'code' })));
expect(p.review_exclude).toMatchObject({ excluded: 1, content: { binary: 1 } });
}, 120_000);
// Value: protects=a branch's .gitattributes cannot get an executable image excluded by making git read it as text, and a
// 64-bit universal Mach-O is an executable image; fails_when=only files git reports as binary under the branch's own
// attributes get the magic check, so an ELF named .png under '*.png diff -binary' is excluded as data, or 0xcafebabf
// is not executable magic; why_new=GST-972 item 3 follow-up (#3910, #3902 outside voice); seam=bin/gstack-review-plan and
// reviewExclude in head mode; no_claim=a NUL-free script under a content extension
test('a branch .gitattributes that makes git read an ELF as text cannot get it excluded; fat64 Mach-O is native', () => {
const repo = makeFixtureRepo({ prefix: 'gstack-exclude-', baseFiles: { '.gstack/ship-policy.json': policyText(['assets/**']) } }); repos.push(repo);
fs.mkdirSync(path.join(repo.work, 'assets'), { recursive: true });
fs.writeFileSync(path.join(repo.work, 'assets/native.png'), Buffer.concat([Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]), Buffer.alloc(24)]));
fs.writeFileSync(path.join(repo.work, 'assets/fat64.dat'), Buffer.concat([Buffer.from([0xca, 0xfe, 0xba, 0xbf]), Buffer.alloc(28)]));
fs.writeFileSync(path.join(repo.work, 'assets/real.gz'), GZ);
commit(repo, { '.gitattributes': '*.png diff -binary\n' });
const p = JSON.parse(plan(repo, ['--json']).out);
const kept = [{ path: 'assets/fat64.dat', reason: 'code' as const }, { path: 'assets/native.png', reason: 'code' as const }];
expect(p.review_exclude.kept_files).toEqual(kept);
expect(p.review_exclude).toMatchObject({ excluded: 1, content: { binary: 1 } });
const head = repo.git('rev-parse', 'HEAD');
const baseSha = repo.git('rev-parse', 'origin/main');
const c = collectCommit(repo.work, baseSha, head);
const x = reviewExclude({ root: repo.work, baseSha, ref: 'origin/main', mergeBase: c.mergeBase!, head, perPath: c.perPath, added: c.added, classRoot: c.root ?? [], gateTagged: new Set(), context: 'test' });
expect(x.state === 'active' && x.kept).toEqual(kept);
expect(x.state === 'active' && x.record.digest).toBe(p.review_exclude.digest);
}, 120_000);
// Value: protects=a NUL byte cannot turn code or a key file into excludable content; fails_when=a NUL-bearing file with a
// source extension outside the old CODE_EXT list (.gradle, .sql, .tf, .groovy), a key store (.p12) or an extensionless key
// is excluded as binary; why_new=GST-972 item 6 (#3852 targeted gate reader at 61803d9); seam=bin/gstack-review-plan;
// no_claim=the list of binary content extensions is complete (an unknown one is kept, never excluded)
test('a NUL-bearing file is binary content only under a known content extension; code and key files stay kept', () => {
const repo = makeFixtureRepo({ prefix: 'gstack-exclude-', baseFiles: { '.gstack/ship-policy.json': policyText(['assets/**']) } }); repos.push(repo);
fs.mkdirSync(path.join(repo.work, 'assets'), { recursive: true });
const nul = (code: string) => Buffer.from(`/* \0 */ ${code}\n`);
const kept = { 'assets/Build.groovy': nul('println "x"'), 'assets/build.gradle': nul('apply plugin: "java"'), 'assets/id_ed25519': nul('openssh-key-v1'), 'assets/main.tf': nul('resource "x" "y" {}'), 'assets/query.sql': nul('DROP TABLE users;'), 'assets/server.p12': Buffer.from([0x30, 0x82, 0, 1, 2, 3]) };
const content = { 'assets/report.pdf': GZ, 'assets/shot.png': GZ };
for (const [rel, bytes] of Object.entries({ ...kept, ...content })) fs.writeFileSync(path.join(repo.work, rel), bytes);
commit(repo, {});
const p = JSON.parse(plan(repo, ['--json']).out);
expect(p.review_exclude.kept_files).toEqual(Object.keys(kept).map(f => ({ path: f, reason: 'code' })));
expect(p.review_exclude).toMatchObject({ excluded: 2, content: { binary: 2 } });
}, 120_000);
// Value: protects=a branch's .gitattributes cannot get a matched file with a credential line excluded by making git call it
// binary, at the plan (live tree) and at the receipt (head blobs); fails_when=the secret-line rule reads only the
// collector's added lines, which git leaves empty for an attribute-binary file, so the file is excluded; why_new=GST-972
// item 5 (#3852 round 3, outside-voice#2); seam=bin/gstack-review-plan and reviewExclude in head mode; no_claim=pregate's gitleaks scan, what readers' own diffs show (they still follow the attributes)
test('a branch .gitattributes marking receipts binary cannot get a credential-line file excluded: the keep rule reads the text', () => {
const repo = makeFixtureRepo({ prefix: 'gstack-exclude-', baseFiles: { '.gstack/ship-policy.json': policyText(['receipts/**']) } }); repos.push(repo);
const secret = '{"cmd": "curl -H $OPENAI_API_KEY"}\n';
commit(repo, { '.gitattributes': 'receipts/** binary\n', 'receipts/s.json': secret, 'receipts/a.b/c.json': secret, 'receipts/a.json': '{"ok": true}\n', 'receipts/a/t.json': '{"ok": true}\n' });
const p = JSON.parse(plan(repo, ['--json']).out);
const kept = [{ path: 'receipts/a.b/c.json', reason: 'secret-line' as const }, { path: 'receipts/s.json', reason: 'secret-line' as const }];
expect(p.review_exclude.kept_files).toEqual(kept);
expect(p.review_exclude).toMatchObject({ excluded: 2, content: { data: 2 } });
const head = repo.git('rev-parse', 'HEAD');
const baseSha = repo.git('rev-parse', 'origin/main');
const c = collectCommit(repo.work, baseSha, head);
const x = reviewExclude({ root: repo.work, baseSha, ref: 'origin/main', mergeBase: c.mergeBase!, head, perPath: c.perPath, added: c.added, classRoot: c.root ?? [], gateTagged: new Set(), context: 'test' });
expect(x.state === 'active' && x.kept).toEqual(kept);
expect(x.state === 'active' && x.record.digest).toBe(p.review_exclude.digest);
}, 120_000);
test('an unavailable gate scan keeps every candidate instead of excluding it', () => {
const repo = census();
const head = repo.git('rev-parse', 'HEAD');
+17
View File
@@ -191,6 +191,23 @@ describe('lib: deriveFromFindings, bindReview order and reviewFreshness', () =>
expect(reviewFreshness(bound, 'd'.repeat(64))?.status).toBe('STALE');
});
// Value: protects=a fix-check row is written with the fields review-read verifies (GEVA-218: the row lacked them, so it read
// UNVERIFIED and finish could not count it); fails_when=the logger accepts a review-fix-check row without completed and
// converged, or consumes the start token while refusing it; seam=bindReview
test('a review-fix-check row without completed and converged is refused and keeps its start token (GST-1130)', () => {
const dir = path.join(home, 'starts', '.review-starts');
fs.mkdirSync(dir, { recursive: true });
const token = '77777777-2222-3333-4444-555555555555';
const w = 'e'.repeat(64);
fs.writeFileSync(path.join(dir, `${token}.json`), JSON.stringify({ skill: 'review-fix-check', repo: '/r', branch: 'b', wtree: w, started_at: 't' }));
const env = { GSTACK_REVIEW_DIR: path.join(home, 'starts'), GSTACK_REVIEW_REPO: '/r', GSTACK_REVIEW_BRANCH: 'b', GSTACK_STAMP_WTREE: w };
for (const row of [{}, { completed: true }, { converged: true }, { completed: 'yes', converged: true }]) {
expect(() => bindReview({ skill: 'review-fix-check', status: 'clean', ...row }, token, env)).toThrow('review-log: a review-fix-check row must carry completed and converged as true or false');
}
expect(fs.existsSync(path.join(dir, `${token}.json`))).toBe(true);
expect(bindReview({ skill: 'review-fix-check', status: 'clean', completed: true, converged: true }, token, env).review_binding.state).toBe('verified');
});
test('reviewFreshness reads findings_open: resolved criticals are CURRENT, claimed issues_found stays conservative', () => {
const w = 'b'.repeat(64);
const base = { skill: 'review', completed: true, converged: true, wtree: w, review_binding: { state: 'verified', start_wtree: w, end_wtree: w } };
+2 -1
View File
@@ -74,6 +74,7 @@ const NO_LOSS_REPLAYS = {
'GST-967': "GST-967 dispatch-superset no-loss check (test/review-depth.test.ts, 'GST-967 dispatch-superset no-loss check'): over 1,440 planReview cases the branch plans exactly what main planned at 1022f4f except the 71 gate-class stops before Step 11, which add only Step 11's adversarial, outside and targeted voices, so no reviewer main dispatches is dropped or re-efforted; the brief bodies lib/review-brief.ts cuts from the ship sections hash to main's; only the lead's repeat-decision and Step 11 instructions changed; reviewer text is unchanged",
'GST-508+GST-464+GST-967': "wave review-53cfb5bfe011 merge of main 68410828f into #3852: ship/sections/review-army.md and adversarial.md are main's GST-967 bytes plus exactly the GST-508+GST-464 lines (the merged file minus main equals the wave minus fc292e22f, line for line), so GST-967's dispatch-superset no-loss check and GST-508's no-loss replay both hold; merged-head checks in GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md",
'GST-508+R51+GST-911+GST-967': "wave review-53cfb5bfe011 merge of main 68410828f into #3852: lib/review-depth.ts is main's GST-967 bytes plus exactly the GST-508+R51+GST-911 lines (the merged file minus main equals the wave minus fc292e22f, line for line); GST-967's dispatch-superset no-loss check (test/review-depth.test.ts) passes on the merged file, and the GST-508+R51+GST-911 checks are rerun at the merged head (GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md)",
'GST-940': "GST-940 verdict no-loss check: svgDrawing reads a style element's opening tag as quoted pairs with ASCII whitespace, walks its body over the raw text, refuses any & in it, reads comments as CSS and refuses a non-ASCII element or attribute name; on 62,805 unique SVG files (21,658 with a style element) it gives the same verdict as main at 4bbad2422 on every file, and the planner over the last 100 first-parent main commits to 4bbad2422 changes no commit's depth, class or risk classes; the only verdicts it changes are the GST-940 cases in test/review-planner-misreads.test.ts; rerun on the merge with main at 8f08cb76a (GST-967 merged), with the same results; rerun on the merge with main at 5f705f367 (#3852 and #3864 merged), with the same results",
} as const;
type Replay = keyof typeof NO_LOSS_REPLAYS;
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
@@ -95,7 +96,7 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' },
'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-depth.ts': { sha: '25050a7a61f81df649d1df0d0237dbf961671356395317c6a527741f5bb56877', replay: 'GST-940' },
'lib/review-risk.ts': { sha: '08464b78a723ee463ca3a28d0abbc4eb8f4f512192061c703743707b66b5a311', replay: 'GST-942' },
};
+21 -1
View File
@@ -125,7 +125,9 @@ describe('GST-911 misread 1: a chart SVG is data, an SVG that can run code is se
// Value: protects=only an SVG made of drawing elements is data: anything that loads, links or declares outside that set stays code;
// fails_when=an external image or stylesheet, an entity declaration, an external DTD, an animation or a CSS url() to another document reads as docs,
// including through a CSS escape or a zero-padded character reference (review pass 1 findings) or behind a stray quote that
// pairs with a later one (review pass 2 finding), or any CSS beyond declarations and url(#id) (Step 11 finding: image-set());
// pairs with a later one (review pass 2 finding), or any CSS beyond declarations and url(#id) (Step 11 finding: image-set()),
// including CSS behind a character-reference </style> or comment, an HTML-only comment end, a quoted > on the style tag, a
// non-ASCII space before > in </style>, or a tag or attribute under a non-ASCII namespace prefix (GST-940);
// seam=collectCommit + depthOfCommitInput
test.each([
['an external image', ' <image href="https://x.test/pixel.png" width="1" height="1"/>'],
@@ -153,11 +155,29 @@ describe('GST-911 misread 1: a chart SVG is data, an SVG that can run code is se
['a style attribute cross-fade()', ' <rect style="fill:cross-fade(url(https://x.test/a.png), url(#m0), 50%)"/>'],
['a style element whose </style> hides in a comment', ' <style>svg{fill:<!-- </style> -->image-set("https://x.test/p.png" 1x)}</style>'],
['a style element whose </style> hides in CDATA', ' <style><![CDATA[ /* </style> */ svg{fill:image-set("https://x.test/p.png" 1x)} ]]></style>'],
['a character-reference </style> in a style element', ' <style>&#60;/style>rect{fill:image-set("https://x.test/p.png" 1x)}</style>'],
['a hex character-reference </style> in a style element', ' <style>&#x3C;/style>rect{fill:image-set("https://x.test/p.png" 1x)}</style>'],
['a character-reference comment in a style element', ' <style>&#60;!-- rect{fill:image-set("https://x.test/p.png" 1x)} --></style>'],
['CSS after an HTML empty comment in a style element', ' <style><!-->rect{fill:image-set("https://x.test/p.png" 1x)}--></style>'],
['CSS after an HTML --!> comment end in a style element', ' <style><!-- --!>rect{fill:image-set("https://x.test/p.png" 1x)} --></style>'],
['a </style> inside a quoted style attribute', ' <style title="></style>">rect{fill:image-set("https://x.test/p.png" 1x)}</style>'],
['an escaped ampersand in a style element', ' <style>text{font-family:"A &amp; B"}</style>'],
['a no-break space before > in </style>', ' <style>a{}</style\u00a0>rect{fill:image-set("https://x.test/p.png" 1x)}</style>'],
['a vertical tab before > in </style>', ' <style>a{}</style\u000b>rect{fill:image-set("https://x.test/p.png" 1x)}</style>'],
['a script under a non-ASCII prefix', ' <g\u00e9:script xmlns:g\u00e9="http://www.w3.org/2000/svg">fetch(1)</g\u00e9:script>'],
['a style under a non-ASCII prefix', ' <g\u00e9:style xmlns:g\u00e9="http://www.w3.org/2000/svg">rect{fill:image-set("https://x.test/p.png" 1x)}</g\u00e9:style>'],
['an external href under a non-ASCII prefix', ' <image x\u00e9:href="https://x.test/p.png" xmlns:x\u00e9="http://www.w3.org/1999/xlink"/>'],
])('%s keeps the SVG code', (_name, line) => {
const fx = fixture();
expect(plan(fx, fx.commit({ 'results/chart.svg': chart(5, line) })).class).toBe('code');
}, 60_000);
// Value: protects=the GST-940 style walker still reads a plain chart style as a drawing: a comment, CDATA and a quoted attribute
// without markup; fails_when=the walker over-refuses them, so chart SVGs lose the light review; seam=svgDrawing
test('a style comment, CDATA and quoted attribute keep a chart a drawing (GST-940)', () => {
expect(svgDrawing(chart(5, ' <style type="text/css" media="all"><!-- plot --><![CDATA[ rect{fill:#000} ]]></style>'))).toBe(true);
});
// Value: protects=a passive edit to an SVG that already carries a script is still code, because the whole file decides, not the
// changed lines; and an SVG code names stays code like any consumed data file; fails_when=either plans as docs; seam=collectCommit
test('an edit inside a scripted SVG, and an SVG code names, stay code', () => {
+2 -1
View File
@@ -7,6 +7,7 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { DATA_LINE, MONEY_LINE, SECRET_LINE, SECURITY_LINE, matchRisk } from '../lib/review-risk';
const ROOT = path.join(import.meta.dir, '..');
@@ -51,7 +52,7 @@ describe('the rules hashes cover every new pattern', () => {
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
const hash = () => {
const r = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
const r = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
expect(r.status, r.stderr).toBe(0);
return r.stdout.trim();
};
+2 -1
View File
@@ -64,7 +64,8 @@ describe('verifyRun', () => {
['run_stale', world({ run: { ...base.run, updated_at: '2026-10-28T10:00:00Z' } })],
['job_missing', world({ jobs: [{ name: 'lint', conclusion: 'success' }] })],
['job_ambiguous', world({ jobs: [{ name: 'full-suite', conclusion: 'success' }, { name: 'full-suite', conclusion: 'success' }] })],
['job_not_success', world({ jobs: [{ name: 'full-suite', conclusion: 'skipped' }] })],
['job_skipped', world({ jobs: [{ name: 'full-suite', conclusion: 'skipped' }] })],
['job_not_success', world({ jobs: [{ name: 'full-suite', conclusion: 'cancelled' }] })],
['artifact_missing', world({ artifact: new Error('no artifact named full-suite-tree') })],
['artifact_mismatch', world({ artifact: { ...(base.artifact as TreeRecord), run_id: 99 } })],
['artifact_mismatch', world({ artifact: { ...(base.artifact as TreeRecord), run_attempt: 2 } })],
+455 -7
View File
@@ -27,14 +27,15 @@ const runs = (f: ShipFx, name: string) => { const p = path.join(f.root, name); r
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 {
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 }>; attempts?: Record<string, 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 } },
jobs: { 7: (over.jobs ?? [{ name: 'suite', conclusion: 'success' }]).map(j => ({ ...j, completed_at: at(5) })), 9: [{ name: 'suite', conclusion: 'failure', completed_at: at(1) }],
...Object.fromEntries(Object.entries(over.attempts ?? {}).map(([k, js]) => [k, js.map(j => ({ ...j, completed_at: at(5) }))])) },
trees: { 7: { run_id: 7, run_attempt: 1, event: run.event, sha: head, ...(run.event === 'pull_request' ? { head_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));
@@ -51,7 +52,7 @@ describe('Step 5 lanes: CI carry and the tree cache', () => {
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, 'free')).toBe('LANE: free carried run=7/1 tests_ran=2 (files)');
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);
@@ -103,7 +104,7 @@ describe('Step 5 lanes: CI carry and the tree cache', () => {
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@')]);
expect([ok.status, ok.stderr]).toEqual([0, expect.stringContaining('tests_ran=2 unit=files 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$/);
});
@@ -151,8 +152,8 @@ describe('finish, push --early and accept (A1)', () => {
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(lane(ok.stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=2 (files)');
expect(lineOf(ok.stdout, 'EVIDENCE')).toMatch(/^EVIDENCE: FRESH label=free exit=0 tests_ran=2 unit=files .* 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);
@@ -192,3 +193,450 @@ describe('gstack-pregate tests stage and gstack-ci-carry', () => {
expect(cli(['bogus']).status).toBe(2);
});
});
describe('GST-1186: pregate guards carry like lanes (founderstar shape: guards only, no lanes)', () => {
// Value: protects=a guard whose exact command the base's ship_lanes_from maps is carried from the verified CI run at this tree, and a guard
// whose exact command already passed at this tree in the ledger is reused, so guard-only repos stop rerunning suites CI or /ship already ran;
// fails_when=check-quick reruns beside a green CI run, play-through reruns after a same-tree ledger pass, or a dirty tree still carries;
// why_new=FOUN-74 (founderstar GST-1184): about 6 minutes of guard suites rerun per /ship; seam=gstack-evidence record-ci and passed; no_claim=CI
const script = (name: string, tests: number) => `printf x >> ../${name}-runs\nprintf ' ${tests} pass\\n 0 fail\\nRan ${tests} tests across 1 file. [1.00ms]\\n' >&2\n`;
const CHECK = 'bash tests/check.sh --quick';
const PLAY = 'bash tests/play-through.sh';
const guard = (id: string, command: string) => ({ id, when: ['src/**'], command, fix: `rerun ${id}` });
const FOUNDERSTAR = {
...BASE,
'.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [CHECK]: { profiles: ['ci/free'], inventory: "printf 'test/a.test.ts\\ntest/b.test.ts\\n'" } },
guards: [guard('check-quick', CHECK), guard('play-through', PLAY), guard('assets', 'bash tests/assets.sh')] }, null, 2) + '\n',
'tests/check.sh': script('check', 4), 'tests/play-through.sh': script('play', 2), 'tests/assets.sh': script('assets', 1),
};
const guards = (f: ShipFx) => f.run(path.join(BIN, 'gstack-pregate'), ['--only', 'guards', '--allow-repo-commands', '--base', 'main']);
const counts = (f: ShipFx) => [runs(f, 'check-runs'), runs(f, 'play-runs'), runs(f, 'assets-runs')];
test('check-quick carries from CI, play-through reuses a same-tree ledger pass, assets runs; a dirty tree carries nothing', () => {
const f = shipFixture(FOUNDERSTAR); fixtures.push(f);
world(f);
const first = guards(f);
expect(first.status, first.stdout + first.stderr).toBe(0);
expect(first.stdout).toContain('GUARD: check-quick carried runs=7/1');
expect(first.stdout).toContain('3 guard(s) passed: check-quick, play-through, assets (1 from an earlier pass at this tree)');
expect(counts(f)).toEqual([0, 1, 1]);
expect(f.run(path.join(BIN, 'gstack-evidence'), ['run', '--label', 'play', '--', PLAY]).status).toBe(0);
const second = guards(f);
expect(second.status).toBe(0);
expect(second.stdout).toMatch(/GUARD: play-through reused label=play source=local ts=\S+/);
expect(counts(f)).toEqual([0, 2, 2]);
f.write({ 'src/b.ts': 'export const b = 3;\n' });
const dirty = guards(f);
expect(dirty.stdout).toMatch(/CI_CARRY: none guard=check-quick \(dirty_tree/);
expect(dirty.stdout).not.toContain('reused');
expect(counts(f)).toEqual([1, 3, 3]);
});
});
describe('GST-1189: a lane carries by named CI jobs instead of an inventory', () => {
// Value: protects=a ship_lanes_from entry that declares { profiles, jobs } carries only when every named job succeeded in the newest attempt
// of the verified run at HEAD's tree, and refuses with a named reason otherwise; fails_when=a typecheck-only lane cannot carry (no test
// files to inventory), or carries beside a missing, skipped or older-attempt job, or another tree's run;
// why_new=gbrain run 41564913688 (tree 13570abc): verify and typecheck passed in CI but no inventory could name their files; seam=fake gh
const VERIFY = 'bun run verify';
const pregateJson = (entry: Record<string, unknown>) => JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [VERIFY]: { profiles: ['ci/free'], ...entry } } }, null, 2) + '\n';
const GBRAIN = { ...BASE, '.gstack/pregate.json': pregateJson({ jobs: ['verify', 'typecheck'] }) };
const ALL = [{ name: 'suite', conclusion: 'success' }, { name: 'verify', conclusion: 'success' }, { name: 'typecheck', conclusion: 'success' }];
const NO_FILES = { files: [], tests: 0 };
const gbrain = () => { const f = shipFixture(GBRAIN); fixtures.push(f); return f; };
const find = (f: ShipFx) => f.run(path.join(BIN, 'gstack-ci-carry'), ['find', '--base', 'main', '--command', VERIFY, '--allow-repo-commands']);
test('every named job green at the same tree carries, with no test files; record-ci records the jobs', () => {
const f = gbrain();
world(f, { jobs: ALL, tree: NO_FILES });
const ok = find(f);
expect([ok.status, ok.stdout]).toEqual([0, expect.stringMatching(/^CI_CARRY: accepted ci-carry tree=[0-9a-f]{12} jobs=verify,typecheck runs=7\/1 profiles=ci\/free /m)]);
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'verify', '--base', 'main', '--', VERIFY]);
expect([rec.status, rec.stderr]).toEqual([0, expect.stringContaining('tests_ran=n/a log=- source=ci runs=7/1')]);
const row = JSON.parse(fs.readFileSync(findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl')[0]!, 'utf8').trim().split('\n').at(-1)!);
expect([row.jobs, row.tests_ran]).toEqual([['verify', 'typecheck'], 'n/a']);
});
test('a missing job refuses as job_missing', () => {
const f = gbrain();
world(f, { jobs: ALL.filter(j => j.name !== 'typecheck'), tree: NO_FILES });
expect(find(f).stdout).toContain('CI_CARRY: none (no_verified_run: ci/free: run 7: job_missing)');
});
test('a skipped job refuses as job_skipped', () => {
const f = gbrain();
world(f, { jobs: ALL.map(j => j.name === 'typecheck' ? { ...j, conclusion: 'skipped' } : j), tree: NO_FILES });
const r = find(f);
expect([r.status, r.stdout]).toEqual([1, expect.stringContaining('CI_CARRY: none (no_verified_run: ci/free: run 7: job_skipped)')]);
});
test('only the newest attempt counts: a rerun that passed carries, a rerun that skipped the job does not', () => {
const f = gbrain();
const failedFirst = ALL.map(j => j.name === 'typecheck' ? { ...j, conclusion: 'failure' } : j);
world(f, { run: { run_attempt: 2 }, tree: { ...NO_FILES, run_attempt: 2 }, attempts: { '7/1': failedFirst, '7/2': ALL } });
expect(find(f).stdout).toMatch(/^CI_CARRY: accepted ci-carry tree=[0-9a-f]{12} jobs=verify,typecheck runs=7\/2 /m);
world(f, { run: { run_attempt: 2 }, tree: { ...NO_FILES, run_attempt: 2 }, attempts: { '7/1': ALL, '7/2': ALL.map(j => j.name === 'typecheck' ? { ...j, conclusion: 'skipped' } : j) } });
expect(find(f).stdout).toContain('CI_CARRY: none (no_verified_run: ci/free: run 7: job_skipped)');
});
test('a run that tested another tree refuses as tree_mismatch', () => {
const f = gbrain();
world(f, { jobs: ALL, tree: { ...NO_FILES, tree: '1'.repeat(40) } });
expect(find(f).stdout).toContain('CI_CARRY: none (no_verified_run: ci/free: run 7: tree_mismatch)');
});
test('an entry declares exactly one of inventory or jobs', () => {
const both = shipFixture({ ...BASE, '.gstack/pregate.json': pregateJson({ jobs: ['verify'], inventory: 'printf x' }) }); fixtures.push(both);
world(both, { jobs: ALL, tree: NO_FILES });
expect(find(both).stdout).toContain('CI_CARRY: none (profiles_unavailable');
const neither = shipFixture({ ...BASE, '.gstack/pregate.json': pregateJson({}) }); fixtures.push(neither);
world(neither, { jobs: ALL, tree: NO_FILES });
expect(find(neither).stdout).toContain('CI_CARRY: none (profiles_unavailable');
});
});
describe('GST-1190: aliases, newer-gstack keys, narrow counts, and proving a mapping without carrying it', () => {
// Value: protects=a lane carries under an exact alias the base declares (never an implicit env-prefix rewrite); a narrow lane never reports
// the whole run's test count; an unknown key under a project floor above this gstack says to upgrade; --simulate proves a head map while
// writing and clearing nothing; carried never lists a row another base's policy mapped;
// fails_when=an env-prefixed lane reruns beside a green run, a 46-test lane records 9677, a proof run's rows land in the PR's own receipt,
// or a branch's own map carries; why_new=HARN-7 (harness-evals #240), ALPH-14 (alphaclaw #310), GEVA-235 (gstack-evals #180), GGST-15; seam=fake gh
const ev = (f: ShipFx, args: string[]) => f.run(path.join(BIN, 'gstack-evidence'), args);
const ledger = (f: ShipFx) => fs.existsSync(path.join(f.state, 'projects')) ? findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl') : [];
const ALL = [{ name: 'suite', conclusion: 'success' }, { name: 'typecheck', conclusion: 'success' }];
test('a declared alias carries like its key; an undeclared env prefix does not', () => {
const TESTS = 'bun test tests/';
const ALIAS = 'env -u ANTHROPIC_API_KEY bun test tests/';
const f = shipFixture({ ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [TESTS]: { profiles: ['ci/free'], inventory: "printf 'test/a.test.ts\\n'", aliases: [ALIAS] } } }) + '\n' }); fixtures.push(f);
world(f);
const rec = ev(f, ['record-ci', '--label', 'tests', '--base', 'main', '--', ALIAS]);
expect([rec.status, rec.stderr]).toEqual([0, expect.stringContaining('tests_ran=1 unit=files log=- source=ci runs=7/1')]);
const find = (command: string) => f.run(path.join(BIN, 'gstack-ci-carry'), ['find', '--base', 'main', '--command', command, '--allow-repo-commands']).stdout;
expect(find(ALIAS)).toMatch(/^CI_CARRY: accepted ci-carry tree=[0-9a-f]{12} files=1 /m);
expect(find('env -i HOME=/tmp bun test tests/')).toContain('CI_CARRY: none (not_mapped');
});
test('an alias that repeats a key is invalid config', () => {
const f = shipFixture({ ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { a: { profiles: ['ci/free'], jobs: ['suite'] }, b: { profiles: ['ci/free'], jobs: ['suite'], aliases: ['a'] } } }) + '\n' }); fixtures.push(f);
world(f);
expect(f.run(path.join(BIN, 'gstack-ci-carry'), ['find', '--base', 'main']).stdout).toContain('CI_CARRY: none (profiles_unavailable');
});
test('a carried narrow lane counts its covered inventory files, never the run total (alphaclaw: 46 tests recorded as 9677)', () => {
const f = fx();
world(f, { tree: { files: ['test/a.test.ts', 'test/b.test.ts', 'test/c.test.ts'], tests: 9677 } });
const rec = ev(f, ['record-ci', '--label', 'guards', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([0, expect.stringContaining('tests_ran=2 unit=files log=- source=ci')]);
expect(rec.stderr).not.toContain('9677');
});
test('a jobs-mapped guard carries in the guards stage (harness-evals: bunx tsc --noEmit) and reports n/a', () => {
const TSC = 'bunx tsc --noEmit';
const f = shipFixture({ ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [TSC]: { profiles: ['ci/free'], jobs: ['typecheck'] } }, guards: [{ id: 'typecheck', when: ['src/**'], command: TSC, fix: 'bunx tsc --noEmit' }] }) + '\n' }); fixtures.push(f);
world(f, { jobs: ALL, tree: { files: [], tests: 0 } });
const g = f.run(path.join(BIN, 'gstack-pregate'), ['--only', 'guards', '--allow-repo-commands', '--base', 'main']);
expect([g.status, g.stdout]).toEqual([0, expect.stringContaining('GUARD: typecheck carried runs=7/1')]);
const row = JSON.parse(fs.readFileSync(ledger(f)[0]!, 'utf8').trim().split('\n').at(-1)!);
expect([row.tests_ran, row.jobs]).toEqual(['n/a', ['typecheck']]);
});
test('an unknown key under a project floor above this gstack says to upgrade; without the floor it stays the generic error', () => {
const conf = JSON.stringify({ future_lanes: {} }) + '\n';
const floor = shipFixture({}, { '.gstack/pregate.json': conf, '.gstack/ship-policy.json': JSON.stringify({ min_gstack_version: '99.0.0.0' }) + '\n' }); fixtures.push(floor);
const newer = floor.run(path.join(BIN, 'gstack-pregate'), ['validate']);
expect(newer.status).not.toBe(0);
expect(newer.stderr).toContain('needs gstack >= 99.0.0.0; upgrade');
expect(newer.stderr).toContain('(PREGATE_GSTACK_TOO_OLD)');
const plain = shipFixture({}, { '.gstack/pregate.json': conf }); fixtures.push(plain);
const typo = plain.run(path.join(BIN, 'gstack-pregate'), ['validate']);
expect([typo.stderr.includes('(PREGATE_CONFIG_INVALID)'), typo.stderr.includes('needs gstack')]).toEqual([true, false]);
});
test('--simulate proves the branch map: every line says SIMULATION, nothing is written, nothing clears, and head policy never carries', () => {
const VERIFY = 'bun run verify';
const head = JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [VERIFY]: { profiles: ['ci/free'], jobs: ['typecheck'] } } }) + '\n';
const f = shipFixture({ '.github/workflows/free.yml': BASE['.github/workflows/free.yml'] }, { '.gstack/pregate.json': head }); fixtures.push(f);
world(f, { jobs: ALL, tree: { files: [], tests: 0 } });
const sim = f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--policy-from', 'head', '--simulate', '--lane', `verify=${VERIFY}`, '--lane', `unit=${UNIT}`]);
const lines = sim.stdout.trim().split('\n');
expect(lines.every(l => l.startsWith('SIMULATION: ')), sim.stdout + sim.stderr).toBe(true);
expect(sim.stdout).toContain('SIMULATION: LANE: verify would carry run=7/1 tests_ran=n/a');
expect(sim.stdout).toContain('SIMULATION: LANE: unit not mapped');
expect([ledger(f), runs(f, 'unit-runs'), fs.existsSync(path.join(f.work, '.gstack', 'tmp', 'pregate.json'))]).toEqual([[], 0, false]);
expect(JSON.parse(ev(f, ['carried', '--json']).stdout)).toEqual([]);
const real = ev(f, ['record-ci', '--label', 'verify', '--base', 'main', '--', VERIFY]);
expect([real.status, real.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: not_mapped: /m)]);
expect(f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--simulate', '--lane', `verify=${VERIFY}`]).status).toBe(2);
});
test('carried lists only rows the receipt base mapped: a proof run against the PR branch never reaches its receipt', () => {
const f = fx();
expect(f.git('push', '-q', 'origin', 'feat').status).toBe(0);
world(f);
expect(ev(f, ['record-ci', '--label', 'free', '--base', 'feat', '--', FREE]).status).toBe(0);
expect(JSON.parse(ev(f, ['carried', '--base', 'main', '--json']).stdout)).toEqual([]);
expect(JSON.parse(ev(f, ['carried', '--base', 'feat', '--json']).stdout)).toEqual([expect.objectContaining({ lane: 'free', run: 7, attempt: 1 })]);
expect(JSON.parse(ev(f, ['carried', '--json']).stdout)).toEqual([]);
});
});
describe('GST-1190: a file whose every test skipped on CI is not carried', () => {
// Value: protects=a lane never carries a test file that ran nothing on the CI runner (Docker images, drive paths, an installed gstack), so
// those tests run here instead of silently dropping; a partly skipped file and an older tree.json without skipped_files carry as before;
// fails_when=harness-evals' 75 runner-skipped tests carry as passed; why_new=HARN-7 (harness-evals #240); seam=fake gh
test('a fully skipped inventory file runs the lane here; a partly skipped one (not listed) and an older tree.json still carry', () => {
const f = fx();
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: inventory_skipped: 1 of 2 inventory file\(s\) skipped every test in CI and the lane declares no per_file runner: test\/b\.test\.ts$/m)]);
const out = preflight(f, [`free=${FREE}`]);
expect(out.stdout).toContain('CI_CARRY: none label=free (inventory_skipped');
expect(runs(f, 'free-runs')).toBe(1);
world(f, { tree: { skipped_files: [] } });
expect(f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]).status).toBe(0);
world(f, { tree: { skipped_files: 'test/b.test.ts' } });
expect(f.run(path.join(BIN, 'gstack-ci-carry'), ['find', '--base', 'main', '--command', FREE, '--allow-repo-commands']).stdout).toContain('artifact_mismatch');
});
test('the tests stage runs a selected file the run skipped entirely, and accepts the rest', () => {
const conf = JSON.stringify({ runner: 'bun test {files}', dependencies: { 'src/**': ['test/a.test.ts', 'test/b.test.ts'] }, run_profiles: { 'ci/free': PROFILE } }) + '\n';
const t = "import { test, expect } from 'bun:test';\ntest('x', () => expect(1).toBe(1));\n";
const f = shipFixture({ ...BASE, '.gstack/pregate.json': conf, 'test/a.test.ts': t, 'test/b.test.ts': t }); fixtures.push(f);
const stage = () => f.run(path.join(BIN, 'gstack-pregate'), ['--stage', 'tests', '--allow-repo-commands', '--base', 'main']);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const skipped = stage();
expect(skipped.stdout + skipped.stderr).toMatch(/REUSE: accepted 1 of 2 selected local test file\(s\) from run .*; 1 still run \(1 skipped every test in the run\)/);
world(f);
expect(stage().stdout + stage().stderr).toMatch(/REUSE: accepted 2 of 2 selected local test file\(s\) from run .*; 0 still run/);
});
});
describe('GST-1190: a path-skipped CI job counts only under an unchanged gate', () => {
// Value: protects=a lane whose CI job the workflow skipped by path is not owed only when the gate job that decided the skip succeeded and the
// workflow and the gate's scripts are byte-identical to origin/<base>; otherwise the branch controlled the skip and the lane runs here;
// fails_when=a branch edits the workflow or the gate script to skip its own typecheck and the lane still passes; why_new=TRUT-7 (truther #1169); seam=fake gh
const TSC = 'bunx tsc --noEmit';
const SKIP_PROFILE = { ...PROFILE, jobs: ['suite'], skippable: { jobs: ['typecheck'], gate: 'changes', scripts: ['scripts/changes.sh'] } };
const WORKFLOW = 'name: Free\non: [pull_request, workflow_dispatch]\njobs:\n changes:\n runs-on: ubuntu-latest\n steps:\n - run: bash scripts/changes.sh\n suite:\n runs-on: ubuntu-latest\n steps:\n - run: bun test\n typecheck:\n needs: changes\n runs-on: ubuntu-latest\n steps:\n - run: bunx tsc --noEmit\n';
const SKIP_BASE = {
'.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': SKIP_PROFILE }, ship_lanes_from: { [TSC]: { profiles: ['ci/free'], jobs: ['typecheck'] } } }) + '\n',
'.github/workflows/free.yml': WORKFLOW,
'scripts/changes.sh': 'echo "typecheck=false" >> "$GITHUB_OUTPUT"\n',
};
const JOBS = (gate: string) => [{ name: 'changes', conclusion: gate }, { name: 'suite', conclusion: 'success' }, { name: 'typecheck', conclusion: 'skipped' }];
const setup = (feat: Record<string, string> = { 'src/b.ts': 'export const b = 2;\n' }, gate = 'success', event = 'pull_request') => {
const f = shipFixture(SKIP_BASE, feat); fixtures.push(f);
world(f, { jobs: JOBS(gate), run: { event }, tree: { files: [], tests: 0, event } });
return f;
};
const lane = (f: ShipFx) => preflight(f, [`tsc=${TSC}`]);
test('a skip the unchanged gate decided is not owed', () => {
const f = setup();
const out = lane(f);
expect(out.stdout).toContain('LANE: tsc not owed: CI skipped at this tree (gate job changes, workflow unchanged) run=7/1');
const row = JSON.parse(fs.readFileSync(findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl')[0]!, 'utf8').trim().split('\n').at(-1)!);
expect([row.tests_ran, row.not_owed]).toEqual(['n/a', 'CI skipped at this tree (gate job changes, workflow unchanged)']);
});
test('a branch that changed the workflow decided its own skip: the lane runs here', () => {
const f = setup({ '.github/workflows/free.yml': WORKFLOW + '# skip more\n' });
expect(lane(f).stdout).toContain('CI_CARRY: none label=tsc (no_verified_run: ci/free: run 7: job_skipped)');
});
test('a branch that changed the gate script decided its own skip: the lane runs here', () => {
const f = setup({ 'scripts/changes.sh': 'echo "typecheck=false" >> "$GITHUB_OUTPUT" # always\n' });
expect(lane(f).stdout).toContain('CI_CARRY: none label=tsc (no_verified_run: ci/free: run 7: job_skipped)');
});
test('a gate job that failed decided nothing: the lane runs here', () => {
const f = setup(undefined, 'failure');
expect(lane(f).stdout).toContain('CI_CARRY: none label=tsc (no_verified_run: ci/free: run 7: job_skipped)');
});
test('only a pull_request run\'s skip counts: a push or dispatch gate diffs against the last push, not the PR base', () => {
const f = setup(undefined, 'success', 'workflow_dispatch');
expect(lane(f).stdout).toContain('CI_CARRY: none label=tsc (no_verified_run: ci/free: run 7: job_skipped)');
});
test('gstack-ci-carry find --policy-from head --simulate checks the branch map, says SIMULATION on every line and writes nothing', () => {
const VERIFY = 'bun run verify';
const head = JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [VERIFY]: { profiles: ['ci/free'], jobs: ['suite'] } } }) + '\n';
const f = shipFixture({ '.github/workflows/free.yml': BASE['.github/workflows/free.yml'] }, { '.gstack/pregate.json': head }); fixtures.push(f);
world(f, { tree: { files: [], tests: 0 } });
const cli = (args: string[]) => f.run(path.join(BIN, 'gstack-ci-carry'), args);
const sim = cli(['find', '--base', 'main', '--policy-from', 'head', '--simulate', '--command', VERIFY, '--allow-repo-commands']);
expect(sim.status).toBe(0);
expect(sim.stdout.trim().split('\n').every(l => l.startsWith('SIMULATION: '))).toBe(true);
expect(sim.stdout).toMatch(/^SIMULATION: CI_CARRY: accepted ci-carry tree=[0-9a-f]{12} jobs=suite /m);
expect(fs.existsSync(path.join(f.state, 'projects')) ? findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl') : []).toEqual([]);
expect(cli(['find', '--base', 'main', '--command', VERIFY, '--allow-repo-commands']).stdout).toContain('CI_CARRY: none (not_mapped');
expect(cli(['find', '--base', 'main', '--simulate']).status).toBe(2);
expect(cli(['wait', '--base', 'main', '--policy-from', 'head', '--simulate']).status).toBe(2);
});
});
describe('GST-1190: a lane carries the covered files and runs only the skipped remainder with its per_file runner', () => {
// Value: protects=a fully skipped CI file runs here through the lane's declared per_file runner while the rest carries, a failing remainder
// carries nothing, and a lane with no runner fails closed by name; fails_when=gstack's 6 runner-skipped files make the whole free suite rerun,
// or a remainder failure still records a carried pass; why_new=HARN-7 decision (GSTA-258, per-file lane carry); seam=fake gh, gstack-evidence record-ci
const PER = (exit: number) => `printf '%s\\n' {files} >> ../perfile-args; printf ' 1 pass\\n 0 fail\\nRan 1 tests across 1 file. [1.00ms]\\n' >&2; exit ${exit}`;
const withRunner = (exit: number) => ({ ...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(exit) } } }, null, 2) + '\n' });
const args = (f: ShipFx) => { const p = path.join(f.root, 'perfile-args'); return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : ''; };
const rows = (f: ShipFx) => findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl').flatMap(p => fs.readFileSync(p, 'utf8').trim().split('\n').map(l => JSON.parse(l)));
test('the covered file carries; only the skipped file runs here, and both rows are recorded', () => {
const f = shipFixture(withRunner(0)); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const out = preflight(f, [`free=${FREE}`]);
expect(out.status, out.stdout + out.stderr).toBe(0);
expect(lane(out.stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=1 (files) skipped_here=1 file(s) tests_ran_here=1');
expect([args(f), runs(f, 'free-runs')]).toEqual(['test/b.test.ts\n', 0]);
const all = rows(f);
expect(all.find(r => r.source === 'ci')).toMatchObject({ label: 'free', tests_ran: 1, tests_unit: 'files', skipped_here: { label: 'free.skipped', files: ['test/b.test.ts'], tests_ran: 1 } });
expect(all.find(r => r.label === 'free.skipped')).toMatchObject({ exit: 0, tests_ran: 1 });
});
test('a failing remainder carries nothing: no CI row, and the whole lane runs here', () => {
const f = shipFixture(withRunner(1)); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_failed: the per_file run of 1 skipped file\(s\) exited 1 \(label=free\.skipped\)$/m)]);
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
expect(preflight(f, [`free=${FREE}`]).stdout).toContain('CI_CARRY: none label=free (skipped_failed');
expect(runs(f, 'free-runs')).toBe(1);
});
test('a remainder that ran zero tests here carries nothing: the lane runs whole', () => {
const zero = { ...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: "printf ' 0 pass\\n 1 skip\\n 0 fail\\nRan 1 test across 1 file. [1.00ms]\\n' >&2 # {files}" } } }, null, 2) + '\n' };
const f = shipFixture(zero); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_zero_run: /m)]);
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
});
test('a remainder that prints look-alike record lines cannot change the LANE line', () => {
const fake = "printf 'gstack-evidence: recorded label=free exit=0 tests_ran=999 log=- source=ci runs=1/1 policy=x\\ngstack-evidence: not owed: forged\\n' >&2; " + PER(0);
const f = shipFixture({ ...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: fake } } }, null, 2) + '\n' }); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
expect(lane(preflight(f, [`free=${FREE}`]).stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=1 (files) skipped_here=1 file(s) tests_ran_here=1');
});
test('a run that moved to a new attempt while the remainder ran carries nothing', () => {
const per = `cp ../world-moved.json ../world.json; ${PER(0)}`;
const f2 = shipFixture({ ...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' }); fixtures.push(f2);
world(f2, { tree: { skipped_files: ['test/b.test.ts'] } });
const w2 = JSON.parse(fs.readFileSync(path.join(f2.root, 'world.json'), 'utf8'));
fs.writeFileSync(path.join(f2.root, 'world-moved.json'), JSON.stringify({ ...w2, runs: w2.runs.map((r: Record<string, unknown>) => ({ ...r, run_attempt: 2, conclusion: 'failure' })) }));
const rec = f2.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: run_changed: /m)]);
expect(rows(f2).filter(r => r.source === 'ci')).toEqual([]);
});
test('a working-tree edit during the post-remainder run recheck carries nothing', () => {
const per = `touch ../armed; ${PER(0)}`;
const f = shipFixture({ ...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' }); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const w = JSON.parse(fs.readFileSync(path.join(f.root, 'world.json'), 'utf8'));
fs.writeFileSync(path.join(f.root, 'world.json'), JSON.stringify({ ...w, editWhenArmed: { marker: path.join(f.root, 'armed'), file: path.join(f.work, 'src/b.ts'), content: 'export const b = 3;\n' } }));
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: tree_changed: /m)]);
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
});
test('per_file is validated: it needs {files} and an inventory entry', async () => {
const { shipLanesErrors } = await import('../lib/ci-carry');
expect(shipLanesErrors({ x: { profiles: ['p'], inventory: 'ls', per_file: 'bun test {files}' } }, ['p'])).toEqual([]);
expect(shipLanesErrors({ x: { profiles: ['p'], inventory: 'ls', per_file: 'bun test' } }, ['p'])).toEqual([expect.objectContaining({ path: 'ship_lanes_from["x"].per_file' })]);
expect(shipLanesErrors({ x: { profiles: ['p'], jobs: ['j'], per_file: 'bun test {files}' } }, ['p'])).toEqual([expect.objectContaining({ path: 'ship_lanes_from["x"].per_file' })]);
});
});
describe('GSTA-258 L6 review fixes: gate inputs, ledger CI rows, simulate consent', () => {
// Value: protects=a branch cannot move a path skip through a gate file gstack failed to compare, a stale CI ledger row never passes a guard
// after record-ci refused, and --simulate runs a branch's inventory only with --allow-repo-commands; fails_when=the l6c-p1 reproductions pass
// again; why_new=l6c-p1 findings (readers 15 and 16); seam=fake gh, gstack-evidence
const TSC = 'bunx tsc --noEmit';
const flow = (gateJob: string, before = '') => `name: Free\non: [pull_request, workflow_dispatch]\njobs:\n${before} changes:\n runs-on: ubuntu-latest\n${gateJob} suite:\n runs-on: ubuntu-latest\n steps:\n - run: bun test\n typecheck:\n needs: changes\n runs-on: ubuntu-latest\n steps:\n - run: bunx tsc --noEmit\n`;
const profile = (scripts: string[] | undefined) => ({ ...PROFILE, jobs: ['suite'], skippable: { jobs: ['typecheck'], gate: 'changes', ...(scripts ? { scripts } : {}) } });
const base = (workflow: string, extra: Record<string, string>, scripts: string[] | undefined = ['scripts/changes.sh']) => ({
'.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': profile(scripts) }, ship_lanes_from: { [TSC]: { profiles: ['ci/free'], jobs: ['typecheck'] } } }) + '\n',
'.github/workflows/free.yml': workflow, 'scripts/changes.sh': 'echo typecheck=false >> "$GITHUB_OUTPUT"\n', ...extra,
});
const JOBS = [{ name: 'changes', conclusion: 'success' }, { name: 'suite', conclusion: 'success' }, { name: 'typecheck', conclusion: 'skipped' }];
const tsc = (b: Record<string, string>, feat: Record<string, string>) => { const f = shipFixture(b, feat); fixtures.push(f); world(f, { jobs: JOBS, run: { event: 'pull_request' }, tree: { files: [], tests: 0, event: 'pull_request' } }); return preflight(f, [`tsc=${TSC}`]).stdout; };
const REFUSED = 'CI_CARRY: none label=tsc (no_verified_run: ci/free: run 7: job_skipped)';
test('a gate script reached through ${{ github.workspace }} is compared', () => {
const b = base(flow(' steps:\n - run: bash scripts/changes.sh && bash ${{ github.workspace }}/scripts/gate.sh\n'), { 'scripts/gate.sh': 'exit 0\n' });
expect(tsc(b, { 'scripts/gate.sh': 'exit 0 # moved\n' })).toContain(REFUSED);
});
test('a gate script under the job\'s working-directory is compared', () => {
const b = base(flow(' defaults:\n run:\n working-directory: tools\n steps:\n - run: bash ../scripts/changes.sh && bash gate2.sh\n'), { 'tools/gate2.sh': 'exit 0\n' });
expect(tsc(b, { 'tools/gate2.sh': 'exit 0 # moved\n' })).toContain(REFUSED);
});
test('a gate that runs a package script compares package.json', () => {
const b = base(flow(' steps:\n - run: bash scripts/changes.sh && bun run detect\n'), { 'package.json': '{"scripts":{"detect":"true"}}\n' });
expect(tsc(b, { 'package.json': '{"scripts":{"detect":"echo typecheck=false"}}\n' })).toContain(REFUSED);
});
test('the gate is the job, not a same-named step in an earlier job', () => {
const b = base(flow(' steps:\n - run: bash scripts/changes.sh && bash scripts/extra.sh\n', ' lint:\n runs-on: ubuntu-latest\n steps:\n - run: echo lint\n name: changes\n'), { 'scripts/extra.sh': 'exit 0\n' });
expect(tsc(b, { 'scripts/extra.sh': 'exit 0 # moved\n' })).toContain(REFUSED);
});
test('a guard whose CI job the unchanged gate skipped says not owed, never carried', () => {
const b = base(flow(' steps:\n - run: bash scripts/changes.sh\n'), {});
const conf = JSON.parse(b['.gstack/pregate.json']);
b['.gstack/pregate.json'] = JSON.stringify({ ...conf, guards: [{ id: 'tsc', when: ['src/**'], command: TSC }] }) + '\n';
const f = shipFixture(b, { 'src/b.ts': 'export const b = 2;\n' }); fixtures.push(f);
world(f, { jobs: JOBS, run: { event: 'pull_request' }, tree: { files: [], tests: 0, event: 'pull_request' } });
const out = f.run(path.join(BIN, 'gstack-pregate'), ['--only', 'guards', '--allow-repo-commands', '--base', 'main']);
expect(out.stdout, out.stdout + out.stderr).toContain('GUARD: tsc not owed: CI skipped at this tree (gate job changes, workflow unchanged) runs=7/1');
});
test('a skippable profile must declare its gate scripts', async () => {
const { runProfileErrors } = await import('../lib/run-evidence');
expect(runProfileErrors({ p: profile(undefined) }, undefined)).toEqual([expect.objectContaining({ path: 'run_profiles["p"].skippable' })]);
expect(runProfileErrors({ p: profile(['scripts/changes.sh']) }, undefined)).toEqual([]);
});
test('passed never counts a CI row: a carry under another base or after a refusal does not pass a guard', () => {
const f = fx();
expect(f.git('push', '-q', 'origin', 'feat').status).toBe(0);
world(f);
const ev = (a: string[]) => f.run(path.join(BIN, 'gstack-evidence'), a);
expect(ev(['record-ci', '--label', 'free', '--base', 'feat', '--', FREE]).status).toBe(0);
const p = ev(['passed', '--', FREE]);
expect([p.status, p.stdout]).toEqual([1, expect.stringMatching(/^NOT_PASSED: /m)]);
});
test('passed: a newer failure of the same command at this tree vetoes the older pass', () => {
const f = fx();
const CMD = "test -e ../fail-flag && exit 1; printf ' 1 pass\\n 0 fail\\nRan 1 tests across 1 file. [1.00ms]\\n' >&2";
const ev = (a: string[]) => f.run(path.join(BIN, 'gstack-evidence'), a);
expect(ev(['run', '--label', 'g', '--', CMD]).status).toBe(0);
expect(ev(['passed', '--', CMD]).stdout).toMatch(/^PASSED: label=g source=local /m);
fs.writeFileSync(path.join(f.root, 'fail-flag'), '');
expect(ev(['run', '--label', 'g', '--', CMD]).status).toBe(1);
expect(ev(['passed', '--', CMD]).stdout).toMatch(/^NOT_PASSED: /m);
});
test('--simulate runs a branch inventory only with --allow-repo-commands', () => {
const INV = "printf x >> ../inv-runs; printf 'test/a.test.ts\\ntest/b.test.ts\\n'";
const head = JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [FREE]: { profiles: ['ci/free'], inventory: INV } } }) + '\n';
const f = shipFixture({ '.github/workflows/free.yml': BASE['.github/workflows/free.yml'] }, { '.gstack/pregate.json': head }); fixtures.push(f);
world(f);
const sim = (extra: string[]) => f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--policy-from', 'head', '--simulate', '--lane', `free=${FREE}`, ...extra]);
const no = sim([]);
expect([no.status, runs(f, 'inv-runs')]).toEqual([1, 0]);
expect(no.stdout).toContain('pass --allow-repo-commands');
const yes = sim(['--allow-repo-commands']);
expect([yes.status, runs(f, 'inv-runs')], yes.stdout).toEqual([0, 1]);
expect(yes.stdout).toContain('SIMULATION: LANE: free would carry run=7/1 tests_ran=2 unit=files');
});
});
+77 -3
View File
@@ -8,7 +8,7 @@ import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
import { extractReceipt, landedStackParent, renderReceiptBlock } from '../lib/ship-receipt';
import { bindShippedReceipt } from '../lib/ship-receipt-depth';
import { cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
@@ -21,7 +21,7 @@ const SUB = "exports.add = (a, b) => a + b;\nexports.sub = (a, b) => a - b;\n";
const RETRY4 = "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n";
/** main <- parent (lib/util.js) <- feature (lib/retry.js, its /ship receipt written against the parent branch). */
function stack(repo: FixtureRepo, parentFiles: Record<string, string> = { 'lib/util.js': SUB }, childFiles: Record<string, string> = { 'lib/retry.js': RETRY4 }): { parent: string; child: string; body: string } {
function stack(repo: FixtureRepo, parentFiles: Record<string, string> = { 'lib/util.js': SUB }, childFiles: Record<string, string> = { 'lib/retry.js': RETRY4 }, base = 'parent'): { parent: string; child: string; body: string } {
repo.git('checkout', '-q', '-b', 'parent');
const parent = commit(repo, parentFiles, 'parent: sub');
repo.git('push', '-q', 'origin', 'parent');
@@ -29,7 +29,7 @@ function stack(repo: FixtureRepo, parentFiles: Record<string, string> = { 'lib/u
repo.git('reset', '-q', '--hard', parent);
const child = commit(repo, childFiles, 'child: four retries');
const body = path.join(repo.root, 'body.md');
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '8', '--base', 'parent', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body]);
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '8', '--base', base, '--version', '1.0.0', '--session-kind', 'unattended', '--body', body]);
expect(w.status).toBe(EXIT.ok);
return { parent, child, body };
}
@@ -166,3 +166,77 @@ describe('carry after a stack parent squash-merges (#3570)', () => {
expect(receiptIn(body).head).toBe(child);
}, 90_000);
});
const CONFIG = ['a = 1', 'b = 2', 'c = 3', 'd = 4', 'e = 5', 'f = 6', 'g = 7', 'h = 8', ''].join('\n');
const configWith = (edits: Record<string, string>) => CONFIG.split('\n').map(l => edits[l] ?? l).join('\n');
describe('carry after a stack parent squash-merges, receipt written against main (GST-1199)', () => {
// Value: protects=a child PR stacked on its parent but targeting main keeps its receipt once the parent squash-merges and
// main is merged in (gstack-evals#178 faced a 45-60M-token gate re-review for it); fails_when=carry compares the child's
// whole range from the old fork point, parent hunks included, with HEAD's diff against a base that now holds them;
// seam=none; no_claim=gh pr edit (--body stands in)
test('the child carries when its range from the landed parent is HEAD\'s whole product diff against main', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const { parent, child, body } = stack(repo, undefined, undefined, 'main');
landOnMain(repo, { 'lib/util.js': SUB });
const head = repo.git('rev-parse', 'HEAD');
const out = carry(repo, body);
expect(out.err).not.toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
expect(out.status).toBe(EXIT.ok);
expect(out.out).toContain(`CARRY: carried from ${child.slice(0, 12)} to ${head.slice(0, 12)}`);
const r = receiptIn(body);
expect(r.head).toBe(head);
expect(r.base).toBe('main');
expect(r.carry!.reason).toBe(`stack parent ${parent.slice(0, 12)} landed + base merge`);
expect(bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '8', [head]).ok).toBe(true);
}, 90_000);
// Value: protects=fail closed: a product edit after the receipt, or a parent that never landed and whose change the
// child's merge dropped, takes a fresh /ship; fails_when=any reviewed ancestor counts as a landed parent, or the edit
// hides behind the shorter range; seam=none; no_claim=-
test('a product edit after the receipt, or a dropped parent that never landed, refuses with RECEIPT_CARRY_PRODUCT_CHANGED', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const { child, body } = stack(repo, undefined, undefined, 'main');
landOnMain(repo, { 'lib/util.js': SUB });
commit(repo, { 'lib/retry.js': RETRY4.replace('4', '5') }, 'child: five');
const edited = carry(repo, body);
expect(edited.status).toBe(EXIT.refused);
expect(edited.out).toContain('CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)');
expect(receiptIn(body).head).toBe(child);
const other = makeFixtureRepo(); repos.push(other);
const s = stack(other, undefined, undefined, 'main');
landOnMain(other, {});
commit(other, { 'lib/util.js': other.git('show', 'origin/main:lib/util.js') + '\n' }, 'drop the parent change');
const dropped = carry(other, s.body);
expect(dropped.status).toBe(EXIT.refused);
expect(dropped.out).toContain('CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)');
expect(receiptIn(s.body).head).toBe(s.child);
}, 120_000);
// Value: protects=a base change next to the child's reviewed hunks gets one fix-check reviewer, not a carry and not a full
// /ship; fails_when=the landed-parent comparison ignores base context or refuses it outright; seam=none; no_claim=-
test('a base change next to the child\'s reviewed hunks routes to RECEIPT_CARRY_SCOPED_REVIEW', () => {
const repo = makeFixtureRepo({ baseFiles: { 'lib/config.js': CONFIG } }); repos.push(repo);
const { child, body } = stack(repo, undefined, { 'lib/config.js': configWith({ 'd = 4': 'd = 40' }) }, 'main');
landOnMain(repo, { 'lib/util.js': SUB, 'lib/config.js': configWith({ 'f = 6': 'f = 60' }) });
const out = carry(repo, body);
expect(out.status).toBe(EXIT.refused);
expect(out.out).toContain('CARRY: refused (RECEIPT_CARRY_SCOPED_REVIEW)');
expect(out.err).toContain('lib/config.js');
expect(receiptIn(body).head).toBe(child);
}, 90_000);
// Value: protects=the receipt never names a stack parent that does not exist; fails_when=a test-first commit with no
// product change counts as landed (any base holds it), so the reason claims a parent and the test range shrinks; seam=none;
// no_claim=-
test('a test-only commit in a branch that was never stacked is no landed parent', () => {
const repo = makeFixtureRepo({ baseFiles: { 'lib/config.js': CONFIG } }); repos.push(repo);
const fork = repo.git('rev-parse', 'HEAD');
commit(repo, { 'test/config.test.js': "const test = require('node:test');\ntest('d', () => {});\n" }, 'test first');
const reviewed = commit(repo, { 'lib/config.js': configWith({ 'd = 4': 'd = 40' }) }, 'fix d');
repo.git('checkout', '-q', 'main');
const mainTip = commit(repo, { 'docs/README.md': '# main moves on\n' }, 'main moves on');
expect(landedStackParent(repo.work, { rangeBase: fork, reviewed, currentBase: mainTip }, ['.', ':(exclude,glob)test/**'])).toBeNull();
}, 60_000);
});
+112
View File
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { comparePath } from '../lib/ship-patch';
import { STAMP_PLACEHOLDERS_KEY_VERSION } from '../lib/ship-policy';
import { extractReceipt, placeReceiptFirst } from '../lib/ship-receipt';
import { cleanup, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
import { lineOf, PREFLIGHT, RELEASE, shipFixture, type ShipFx } from './helpers/ship-bin-fixture';
@@ -266,6 +267,37 @@ describe('a restamp after a lost slot (GST-895, GBRA-218, gbrain #8134)', () =>
const base = kept([['0.9.0.0', 'old'], ['1.0.1.0', 'base'], ['1.0.0.0', 'mine']]);
expect([base.status, lineOf(base.stdout, 'STOP')]).toEqual([3, behaviorStop]);
});
// Value: protects=gbrain's restamp of the PR's own `since: 'TBD'` row to the new version routes as metadata when the
// policy declares TBD in stamp_placeholders (GST-1026, gbrain #8444); an undeclared placeholder, a base row moved the
// same way, a duplicated base copy, or a line no stamp_pattern matches still stops; fails_when=stampedFiles reads the
// move as behavior (gbrain #8444) or accepts it off the branch's own rows; seam=lib/ship-bin-common.ts stampedFiles
test('a declared placeholder on the branch row is metadata; undeclared, a base row or no template stops (GST-1026)', () => {
const declared = (placeholders: string[] | undefined, templates = ["since: '{version}'"]) => JSON.stringify({ stamp_files: ['VERSION', 'src/notice.ts'], stamp_patterns: templates,
...(placeholders ? { stamp_placeholders: placeholders, min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION } : {}) });
const restamped = (pol: string, baseRows: [string, string][], ownRows: [string, string][], rows: [string, string][]) => {
const f = fx({ '.gstack/ship-policy.json': pol, VERSION: '1.0.0.0\n', 'src/notice.ts': notice(...baseRows) },
{ VERSION: '1.0.1.0\n', 'src/notice.ts': notice(...baseRows, ...ownRows) });
const reviewed = wtree(f);
const post = snapshot(f);
f.write({ VERSION: '1.0.2.0\n', 'src/notice.ts': notice(...rows) }); f.commit('restamp: TBD rows to 1.0.2.0');
return finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
};
const base: [string, string][] = [['0.9.0.0', 'old'], ['TBD', 'base']];
const metadata = / behavior=0 docs=0 metadata=2 tests=0 /;
const own = restamped(declared(['TBD']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
expect(lineOf(own.stdout, 'ROUTE'), own.stdout).toEqual(expect.stringMatching(metadata));
const undeclared = restamped(declared(undefined), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
expect([undeclared.status, lineOf(undeclared.stdout, 'STOP')]).toEqual([3, behaviorStop]);
const other = restamped(declared(['WIP']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
expect([other.status, lineOf(other.stdout, 'STOP')]).toEqual([3, behaviorStop]);
const baseRow = restamped(declared(['TBD']), base, [['TBD', 'mine']], [['0.9.0.0', 'old'], ['1.0.2.0', 'base'], ['1.0.2.0', 'mine']]);
expect([baseRow.status, lineOf(baseRow.stdout, 'STOP')]).toEqual([3, behaviorStop]);
const dup = restamped(declared(['TBD']), base, [['TBD', 'base']], [['0.9.0.0', 'old'], ['1.0.2.0', 'base'], ['TBD', 'base']]);
expect([dup.status, lineOf(dup.stdout, 'STOP')]).toEqual([3, behaviorStop]);
const noTemplate = restamped(declared(['TBD'], ['version: {version}']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
expect([noTemplate.status, lineOf(noTemplate.stdout, 'STOP')]).toEqual([3, behaviorStop]);
});
});
describe('adjacent base edits (GST-384, #3631, founderstar#441)', () => {
@@ -757,3 +789,83 @@ describe('finish with an owner-accepted edit after a fix-check (GST-965, GBRA-27
});
});
// Value: protects=a granted fix-check's clean read clears the paths it reviewed at Step 16, a gate-edited test included, so
// finish does not send them back through Steps 5–11.5 (GEVA-218, gstack-evals#168); fails_when=finish routes a path inside
// the fix-check's range as behavior, counts a path edited after the fix-check, or counts a row that is not a completed clean
// fix-check with a range base; seam=finish's route reading the review log; no_claim=Step 11.5's binding
describe('finish after a completed fix-check (GST-1130, GEVA-218)', () => {
const TEST_V1 = "import { expect, test } from 'bun:test';\ntest('a', () => { expect(2).toBe(2); });\n";
const TEST_V2 = "import { expect, test } from 'bun:test';\ntest('a', () => { expect(2).toBe(2); }, 1_000);\ntest('b finishes fast', () => { expect(1 + 1).toBe(2); }, 1_000);\n";
const logFixCheck = (f: ShipFx, row: Record<string, unknown>) => {
const start = f.run(path.join(BIN, 'gstack-review-log'), ['--start', 'review-fix-check']);
expect(start.status).toBe(0);
return f.run(path.join(BIN, 'gstack-review-log'), [JSON.stringify({ skill: 'review-fix-check', timestamp: '2026-11-02T00:00:00Z', via: 'ship', review_scope: 'frozen', status: 'clean', issues_found: 0, critical: 0, ...row }), '--finish', start.stdout.trim()]);
};
const grant = (f: ShipFx, stopHead: string) => {
const a = f.run(path.join(BIN, 'gstack-approvals'), ['add', '--ask', `Owner grant: one scoped fix-check after round 2 (review_loop_grant ${stopHead.slice(0, 12)})`, '--words', 'one scoped fix-check after round 2', '--by', 'GStack Minder', '--scope', 'this /ship invocation', '--json'], { GSTACK_APPROVALS_MIRROR: '0' });
expect(a.status).toBe(0);
return JSON.parse(a.stdout).added[0] as string;
};
const afterFixCheck = () => {
const f = fx({ 'src/x.ts': 'export const x = 1;\n' }, { 'src/x.ts': 'export const x = 2;\n', 'test/x.test.ts': TEST_V1 });
const passHead = f.git('rev-parse', 'HEAD').stdout.trim();
const reviewed = wtree(f);
f.write({ 'src/x.ts': 'export const x = 3;\n', 'test/x.test.ts': TEST_V2 }); f.commit('fix the round-2 blocker and bound its case');
return { f, passHead, fixHead: f.git('rev-parse', 'HEAD').stdout.trim(), reviewed, post: snapshot(f) };
};
test('paths inside a completed clean fix-check range count as reviewed, a gate-edited test included; a later edit still stops', () => {
const { f, passHead, fixHead, reviewed, post } = afterFixCheck();
const plain = finish(f, reviewed, post);
expect([plain.status, plain.stdout.includes('STOP: behavior, tests or build inputs changed since review (src/x.ts,test/x.test.ts)')]).toEqual([3, true]);
const id = grant(f, passHead);
expect(logFixCheck(f, { completed: true, converged: true, range_base: passHead }).status).toBe(0);
const r = finish(f, reviewed, post);
expect(lineOf(r.stdout, 'FIX_CHECK')).toBe(`FIX_CHECK: reviewed src/x.ts,test/x.test.ts (fix-check at ${fixHead.slice(0, 12)}, range from ${passHead.slice(0, 12)}, grant ${id})`);
expect(lineOf(r.stdout, 'ROUTE')).toMatch(/ behavior=0 docs=0 metadata=0 tests=0 base=unknown context=0 fix_checked=2$/);
expect([r.status, lineOf(r.stdout, 'EVIDENCE')]).toEqual([0, expect.stringMatching(/^EVIDENCE: FRESH label=tests /)]);
f.write({ 'src/x.ts': 'export const x = 4;\n' }); f.commit('an edit after the fix-check');
const late = finish(f, reviewed, post);
expect(lineOf(late.stdout, 'FIX_CHECK')).toStartWith('FIX_CHECK: reviewed test/x.test.ts (');
expect([late.status, late.stdout.includes('STOP: behavior, tests or build inputs changed since review (src/x.ts)')]).toEqual([3, true]);
});
test('a clean fix-check row with no recorded review_loop_grant for a stop head in its range is not counted', () => {
const { f, passHead, reviewed, post } = afterFixCheck();
expect(logFixCheck(f, { completed: true, converged: true, range_base: passHead }).status).toBe(0);
const ungranted = finish(f, reviewed, post);
expect(lineOf(ungranted.stdout, 'FIX_CHECK')).toBe('FIX_CHECK: not counted (no approved review_loop_grant names a stop head inside its range_base..commit)');
expect([ungranted.status, ungranted.stdout.includes('STOP: behavior, tests or build inputs changed since review (src/x.ts,test/x.test.ts)')]).toEqual([3, true]);
grant(f, mergeBase(f));
const elsewhere = finish(f, reviewed, post);
expect([elsewhere.status, lineOf(elsewhere.stdout, 'FIX_CHECK')]).toEqual([3, 'FIX_CHECK: not counted (no approved review_loop_grant names a stop head inside its range_base..commit)']);
});
test('a fix-check row that did not finish clean, or names no range base, is not counted', () => {
const { f, passHead, reviewed, post } = afterFixCheck();
expect(logFixCheck(f, { completed: false, converged: false, range_base: passHead }).status).toBe(0);
const incomplete = finish(f, reviewed, post);
expect(lineOf(incomplete.stdout, 'FIX_CHECK')).toBe('FIX_CHECK: not counted (missing start capture or incomplete/nonconverged pass)');
expect([incomplete.status, incomplete.stdout.includes('STOP: behavior, tests or build inputs changed since review (src/x.ts,test/x.test.ts)')]).toEqual([3, true]);
expect(logFixCheck(f, { completed: true, converged: true }).status).toBe(0);
const unranged = finish(f, reviewed, post);
expect(lineOf(unranged.stdout, 'FIX_CHECK')).toBe('FIX_CHECK: not counted (the row names no range_base commit on this branch)');
expect(unranged.status).toBe(3);
const fork = mergeBase(f);
expect(logFixCheck(f, { completed: true, converged: true, range_base: fork }).status).toBe(0);
const early = finish(f, reviewed, post);
expect(lineOf(early.stdout, 'FIX_CHECK')).toBe(`FIX_CHECK: not counted (range_base ${fork.slice(0, 12)} does not hold the reviewed tree)`);
expect(early.status).toBe(3);
});
test('a fix-check logged on uncommitted content is not counted', () => {
const { f, passHead, reviewed, post } = afterFixCheck();
f.write({ 'src/x.ts': 'export const x = 9;\n' });
expect(logFixCheck(f, { completed: true, converged: true, range_base: passHead }).status).toBe(0);
f.git('checkout', '--', 'src/x.ts');
const r = finish(f, reviewed, post);
expect(lineOf(r.stdout, 'FIX_CHECK')).toBe('FIX_CHECK: not counted (the row was logged on content no branch commit holds)');
expect(r.status).toBe(3);
});
});
@@ -0,0 +1,4 @@
{
"bytes": 14,
"why": "GST-1138: ship names the push guards in Step 17 and the early push"
}
+112
View File
@@ -0,0 +1,112 @@
/**
* A bare --base <branch> reads origin's tracking branch, not a stale local branch (GST-1036). Capy clones and linked
* worktrees never move the local base branch, so `--base main` there merged against an old local main: review and ship
* bins then counted upstream commits the branch only inherited (a docs diff planned at gate depth, banned terms and
* prose findings in lines the branch never wrote). Every such bin resolves the base through resolveBase
* (lib/gate-diff/git.ts) and names the ref it used.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { BANNED_TERMS_SCHEMA } from '../lib/banned-terms';
import { resolveCommit } from '../lib/pr-shots';
const BIN = path.join(import.meta.dir, '..', 'bin');
const dirs: string[] = [];
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
function fixture() {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'stale-base-')));
dirs.push(root);
const work = path.join(root, 'work');
const env = { ...process.env, GSTACK_STATE_ROOT: path.join(root, 'state'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig') };
fs.writeFileSync(path.join(root, 'gitconfig'), '[commit]\n\tgpgsign = false\n');
const sh = (cwd: string, c: string) => { const r = spawnSync('bash', ['-c', c], { cwd, env, encoding: 'utf8', timeout: 60_000 }); if (r.status !== 0) throw new Error(`${c}: ${r.stderr}`); return r.stdout.trim(); };
sh(root, 'git init -q --bare -b main origin.git && git clone -q origin.git work 2>/dev/null');
fs.mkdirSync(path.join(work, '.gstack'));
fs.writeFileSync(path.join(work, '.gstack', 'banned-terms.json'), JSON.stringify({ $schema: BANNED_TERMS_SCHEMA, terms: ['frobnicate'] }) + '\n');
fs.writeFileSync(path.join(work, 'README.md'), '# t\n');
sh(work, 'git add -A && git commit -qm base && git push -q origin main');
sh(root, "git clone -q origin.git other && cd other && mkdir docs && printf 'Upstream notes frobnicate the parser.\\n\\nShowcasing vibrant features.\\n' > docs/up.md && git add -A && git commit -qm upstream && git push -q origin main");
sh(work, "git fetch -q origin && git checkout -qb feat origin/main && mkdir src && echo 'export const v = 1;' > src/v.ts && git add -A && git commit -qm feat");
const origin = sh(work, 'git rev-parse refs/remotes/origin/main');
expect(sh(work, 'git rev-parse refs/heads/main')).not.toBe(origin);
const run = (bin: string, args: string[]) => { const r = spawnSync(path.join(BIN, bin), args, { cwd: work, env, encoding: 'utf8', timeout: 180_000 }); return { status: r.status, out: r.stdout ?? '', err: r.stderr ?? '' }; };
return { root, work, sh, run, origin };
}
const NOTE = 'local main is behind origin/main; base is origin/main';
describe('a bare --base reads origin/<base> over a stale local branch (GST-1036)', () => {
// Value: protects=review-plan names the base ref its plan was computed against, so a stale-base plan shows in its output;
// fails_when=review-plan prints no REVIEW_BASE line, or names the local main while origin/main is ahead, or re-qualifies origin/main;
// why_new=GST-1036 (#3882 planned a docs diff at full depth against local main 319ef500); seam=bin/gstack-review-plan, lib/review-collect.ts collectLive
test('gstack-review-plan prints the ref it used and why', () => {
const { run, origin } = fixture();
const bare = run('gstack-review-plan', ['--base', 'main']);
expect(bare.status, bare.out + bare.err).toBe(0);
expect(bare.out).toContain(`REVIEW_BASE: origin/main ${origin.slice(0, 12)}\n`);
expect(bare.err).toContain(`gstack-review-plan: ${NOTE}`);
const json = run('gstack-review-plan', ['--base', 'main', '--json']);
expect(JSON.parse(json.out)).toMatchObject({ paths: ['src/v.ts'], base: { ref: 'origin/main', sha: origin, local: 'behind' } });
const qualified = run('gstack-review-plan', ['--base', 'origin/main']);
expect(qualified.out).toContain(`REVIEW_BASE: origin/main ${origin.slice(0, 12)}\n`);
expect(qualified.err).not.toContain('base is');
}, 240_000);
// Value: protects=banned-terms scans only the lines the branch adds; fails_when=it merges against the stale local main and reports a term an upstream commit added;
// why_new=GST-1036 sibling bin (/review passes a bare <base>); seam=bin/gstack-banned-terms
test('gstack-banned-terms check ignores inherited upstream lines', () => {
const { run } = fixture();
for (const base of ['main', 'origin/main']) {
const r = run('gstack-banned-terms', ['check', '--base', base]);
expect(r.status, r.out + r.err).toBe(0);
expect(r.out).toContain('BANNED_TERMS: ok');
expect(r.err).toContain(`gstack-banned-terms: ${NOTE}`);
}
}, 120_000);
// Value: protects=without a remote-tracking twin the local base still stands, so the origin preference never hides lines;
// fails_when=the fix drops the local fallback; why_new=negative control for GST-1036; seam=bin/gstack-banned-terms
test('no origin: gstack-banned-terms keeps the local main and reports what the branch carries over it', () => {
const { sh, work, run } = fixture();
sh(work, 'git remote remove origin');
const r = run('gstack-banned-terms', ['check', '--base', 'main']);
expect(r.status, r.out + r.err).toBe(1);
expect(r.out).toContain('frobnicate');
expect(r.err).not.toContain('base is');
}, 120_000);
// Value: protects=prose-lint --base lints only the lines the branch adds; fails_when=a bare base merges against the stale local main;
// why_new=GST-1036 sibling bin; seam=lib/prose-lint.ts addedOnly
test('gstack-prose-lint --base main skips a file only upstream changed; origin/main is read as given', () => {
const { run } = fixture();
for (const base of ['main', 'origin/main']) {
const r = run('gstack-prose-lint', ['docs/up.md', '--base', base, '--gate']);
expect(r.status, r.out + r.err).toBe(0);
expect(r.out).not.toContain('PROSE:');
}
}, 120_000);
// Value: protects=the docs-audit snapshot selects only the branch's files; fails_when=--base main resolves to the stale local main and selects upstream docs;
// why_new=GST-1036 sibling bin; seam=bin/gstack-docs-candidate
test('gstack-docs-candidate snapshot --base main records origin/main and selects only the branch', () => {
const { root, run, origin } = fixture();
const r = run('gstack-docs-candidate', ['snapshot', '--out', path.join(root, 'c.json'), '--audit-id', 'a1', '--mode', 'read-only', '--base', 'main']);
expect(r.status, r.out + r.err).toBe(0);
expect(r.out).toContain(`base=${origin} `);
expect(r.out).toContain('committed: "src/v.ts"\n');
expect(r.err).toContain(`gstack-docs-candidate: ${NOTE}`);
}, 120_000);
// Value: protects=the PR screenshot's before side is origin's base, not a stale local copy; fails_when=resolveCommit reads refs/heads/main first;
// why_new=GST-1036 sibling bin; seam=lib/pr-shots.ts resolveCommit
test('gstack-pr-shots --base main reads origin/main; a sha resolves as given', () => {
const { work, sh, origin } = fixture();
expect(resolveCommit(work, 'main')).toBe(origin);
const local = sh(work, 'git rev-parse refs/heads/main');
expect(resolveCommit(work, local)).toBe(local);
}, 120_000);
});
+30
View File
@@ -121,6 +121,36 @@ describe('tree receipt: stamps, stamp-sensitive gates and negative fixtures', ()
expect(stampOnlyLine("mine since: '1.2.3.0'", "mine since: '1.2.4.0'", '1.2.4.0', '1.2.4.0', tpl, mine, 5)).toBe(true);
});
// Value: protects=a declared placeholder on the branch's own row moves to the new version as a stamp, only inside a
// stamp_patterns template (gbrain's `since: 'TBD'` row, GST-1026); fails_when=stampOnlyLine reads the move as a hand
// edit, or accepts an undeclared placeholder, a base row, a duplicated base line, a built-in form or a line no
// template matches; seam=lib/stamp-only.ts stampOnlyLine placeholders
test('a declared placeholder on a branch row moves to the new version inside a stamp_patterns template, and nowhere else (GST-1026)', () => {
const tpl = ["since: '{version}'"];
const row = " { since: 'TBD', id: 'mine' },";
const moved = " { since: '0.60.553.0', id: 'mine' },";
const side = (r: string, at = 10) => ({ fork: '0.60.552.0', added: new Set([r]), at: new Set([at]) });
const line = (r: string, a: string, opts: { templates?: string[]; at?: number; placeholders?: string[]; oldV?: string } = {}) =>
stampOnlyLine(r, a, opts.oldV ?? '0.60.552.0', '0.60.553.0', opts.templates ?? tpl, side(r), opts.at ?? 10, opts.placeholders ?? ['TBD']);
expect(line(row, moved)).toBe(true);
expect(line(row, moved, { oldV: '0.60.553.0' })).toBe(true);
expect(line(row, moved, { placeholders: [] })).toBe(false);
expect(line(row, moved, { placeholders: ['WIP'] })).toBe(false);
expect(line(" { since: 'WIP', id: 'mine' },", moved)).toBe(false);
expect(line(row, moved, { at: 2 })).toBe(false);
expect(stampOnlyLine(row, moved, '0.60.552.0', '0.60.553.0', tpl, undefined, undefined, ['TBD'])).toBe(false);
expect(stampOnlyLine(row, moved, '0.60.552.0', '0.60.553.0', tpl, side(" { since: 'TBD', id: 'other' },"), 10, ['TBD'])).toBe(false);
expect(line(row, moved, { templates: [] })).toBe(false);
expect(line(" { release: 'TBD', id: 'mine' },", " { release: '0.60.553.0', id: 'mine' },")).toBe(false);
expect(line(' { since: TBD, id: mine },', ' { since: 0.60.553.0, id: mine },')).toBe(false);
expect(line(row, " { since: '0.60.553.0', id: 'yours' },")).toBe(false);
expect(line(row, " { since: '0.60.554.0', id: 'mine' },")).toBe(false);
const both = " { since: 'TBD', note: 'Added in v0.60.552.0' },";
expect(line(both, " { since: '0.60.553.0', note: 'Added in v0.60.553.0' },")).toBe(true);
expect(line(both, " { since: '0.60.553.0', note: 'Added in v0.60.551.0' },")).toBe(false);
expect(line(' // TBD: tidy', ' // 0.60.553.0: tidy', { templates: ['{version}'] })).toBe(false);
});
test('negative fixture: a dependency pin equal to the old version is `changed`, not a stamp, even inside stamp_paths', () => {
const repo = fixture();
const g = gated(repo);
+1 -1
View File
@@ -148,7 +148,7 @@ test('every windows-smoke test is Windows-safe, seeded fast, and says why it is
test('no Windows job restores or saves the bun install cache', () => {
// GSTA-203 PR 1: a cold install (16-17 s) beat restoring the ~450 MB
// windows-bun entry (33-42 s), and PR merge-ref saves filled the 10 GB quota.
for (const file of ['windows-free-tests.yml', 'windows-smoke.yml', 'windows-setup-e2e.yml']) {
for (const file of ['windows-free-tests.yml', 'windows-smoke.yml', 'windows-setup-e2e.yml', 'windows-scoped-tests.yml']) {
for (const [name, job] of Object.entries<any>(load(file).jobs)) {
const caches = (job.steps ?? []).filter((step: any) => step.uses?.startsWith('actions/cache'));
expect({ file, job: name, caches }).toEqual({ file, job: name, caches: [] });
+311
View File
@@ -0,0 +1,311 @@
/**
* Windows checks by path (ship-90-90 plan D, GST-1135). Pins three things:
*
* 1. windows-smoke.yml stays the unfiltered Windows check on every pull
* request.
* 2. windows-setup-e2e.yml starts only on install paths, and its
* windows-scope job keeps the two Windows jobs off a pull request whose
* only install change is package.json's version line (the merge-train
* stamp).
* 3. windows-scoped-tests.yml runs the changed Windows-safe test files when
* a pull request touches path, file-name or process code. Its filter must
* catch #3758 (/freeze's check-freeze.sh, a hook path) and #3829 (a test
* that spawned Bun with --config=/dev/null), and pass over a docs-only
* and a pure-logic change. Its aggregate passes when nothing is in scope,
* so a skipped run still reports success.
*
* The filter and scope steps run here as the runner would run them: bash in a
* two-commit git repository, with HEAD^1 as the pull request's base.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { parseWorkflow } from '../lib/pregate/workflows';
import { collectFreeTestFiles, curateWindowsSafe } from '../scripts/test-free-shards';
const ROOT = path.resolve(import.meta.dir, '..');
const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const SCOPED_FILE = 'windows-scoped-tests.yml';
const SETUP_FILE = 'windows-setup-e2e.yml';
const scopedText = read(`.github/workflows/${SCOPED_FILE}`);
const setupText = read(`.github/workflows/${SETUP_FILE}`);
const scoped = Bun.YAML.parse(scopedText) as any;
const setupE2e = Bun.YAML.parse(setupText) as any;
const smoke = Bun.YAML.parse(read('.github/workflows/windows-smoke.yml')) as any;
const toShell = (p: string) => p.split(path.sep).join('/');
const CAN_RUN = !!Bun.which('bash') && !!Bun.which('git');
const PKG = (version: string, deps = '"a": "1.0.0"') => `{\n "name": "gstack",\n "version": "${version}",\n "dependencies": {\n ${deps}\n }\n}\n`;
function git(cwd: string, args: string[]): string {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 30_000 });
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
return r.stdout;
}
/** A repository with a base commit and one change on top: HEAD^1 plays the pull request's base, as on the merge ref. */
function repo(base: Record<string, string>, change: Record<string, string>): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'windows-scope-'));
const write = (files: Record<string, string>) => {
for (const [rel, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), text);
}
};
git(dir, ['init', '-q']);
write(base);
git(dir, ['add', '-A']);
git(dir, ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@gstack.test', 'commit', '-q', '-m', 'base']);
write(change);
git(dir, ['add', '-A']);
git(dir, ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@gstack.test', 'commit', '-q', '-m', 'change']);
return dir;
}
/** GITHUB_OUTPUT lines, including name<<DELIMITER blocks. */
function parseOutputs(text: string): Record<string, string> {
const out: Record<string, string> = {};
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const block = /^([\w-]+)<<(.+)$/.exec(lines[i]!);
if (block) {
const end = lines.indexOf(block[2]!, i + 1);
out[block[1]!] = lines.slice(i + 1, end).join('\n');
i = end;
} else if (lines[i]!.includes('=')) {
const at = lines[i]!.indexOf('=');
out[lines[i]!.slice(0, at)] = lines[i]!.slice(at + 1);
}
}
return out;
}
interface StepRun { status: number | null; stderr: string; outputs: Record<string, string>; summary: string; temp: string }
const TEMPS: string[] = [];
afterAll(() => { for (const dir of TEMPS) fs.rmSync(dir, { recursive: true, force: true }); });
const newTemp = () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'windows-scope-temp-')); TEMPS.push(dir); return dir; };
/** Run one workflow step's script under bash with a fresh GITHUB_OUTPUT and summary; temp is RUNNER_TEMP, kept for a later step. */
function runStep(script: string, cwd: string, env: Record<string, string> = {}, temp = newTemp()): StepRun {
const output = path.join(temp, 'github-output');
const summary = path.join(temp, 'github-summary');
fs.writeFileSync(output, '');
fs.writeFileSync(summary, '');
const r = spawnSync('bash', ['-c', script], {
cwd, encoding: 'utf8', timeout: 30_000,
env: { PATH: process.env.PATH ?? '', HOME: process.env.HOME ?? temp, GITHUB_EVENT_NAME: 'pull_request', RUNNER_TEMP: toShell(temp), GITHUB_OUTPUT: toShell(output), GITHUB_STEP_SUMMARY: toShell(summary), ...env },
});
return { status: r.status, stderr: r.stderr, outputs: parseOutputs(fs.readFileSync(output, 'utf8')), summary: fs.readFileSync(summary, 'utf8'), temp };
}
const step = (job: any, id: string) => job.steps.find((s: any) => s.id === id);
const filterScript: string = step(scoped.jobs['windows-scope'], 'filter').run;
const selectScript: string = step(scoped.jobs['windows-scope'], 'select').run;
const setupScopeScript: string = step(setupE2e.jobs['windows-scope'], 'scope').run;
// #3758 (v1.92.9.0, /freeze checks where an edit really lands): its changed files. Its Windows failure was in
// test/hook-scripts.test.ts, from check-freeze.sh's path handling (run 41243040943).
const PR_3758 = ['CHANGELOG.md', 'VERSION', 'agents-digest/gstack-AGENTS.md', 'careful/bin/hook-extract.sh', 'freeze/SKILL.md', 'freeze/SKILL.md.tmpl',
'freeze/bin/check-freeze.sh', 'package.json', 'test/hook-scripts.test.ts'];
// #3829 (v1.92.61.0, risk classes cover child_process): its changed files, none named for paths or processes. Its
// Windows failure (nightly run 41420490350) was test/review-risk-linear.test.ts, a file it added, spawning Bun with
// --config=/dev/null: EINVAL on native Windows.
const PR_3829 = ['CHANGELOG.md', 'VERSION', 'agents-digest/gstack-AGENTS.md', 'docs/review-policy.md', 'lib/review-collect.ts', 'lib/review-depth.ts',
'lib/review-risk.ts', 'lib/review-targeted.ts', 'package.json', 'test/fixtures/review-risk/gbrain-evals-289.json', 'test/helpers/free-fixtures-data.ts',
'test/review-packets-removed.test.ts', 'test/review-risk-linear.test.ts', 'test/review-risk-routing.test.ts'];
const LINE_3829 = " const r = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });";
/** Every file of a pull request with a neutral one-line edit, package.json as a version bump, plus the given lines. */
function prRepo(files: string[], lines: Record<string, string> = {}) {
const base: Record<string, string> = {};
const change: Record<string, string> = {};
for (const file of files) {
base[file] = file === 'package.json' ? PKG('1.0.0') : 'unchanged\n';
change[file] = file === 'package.json' ? PKG('1.0.1') : `unchanged\nedited\n${lines[file] ?? ''}\n`;
}
return repo(base, change);
}
describe('windows-smoke stays on every pull request', () => {
test('no paths filter, no job condition, one job', () => {
expect(smoke.on.pull_request).toEqual({ branches: ['main'] });
expect(smoke.on.push).toEqual({ branches: ['main'] });
expect(Object.keys(smoke.jobs)).toEqual(['windows-smoke']);
expect(smoke.jobs['windows-smoke'].if).toBeUndefined();
expect(smoke.jobs['windows-smoke'].needs).toBeUndefined();
});
test('no Windows job that can skip is a required check, and the scoped aggregate always reports', () => {
const required = JSON.parse(read('.gstack/ship-policy.json')).changelog_fragments.required_checks as string[];
expect(required).toContain('windows-smoke');
for (const name of ['windows-scope', 'windows-setup', 'held-install-journey', 'windows-scoped-shard', 'Windows Setup E2E']) expect(required).not.toContain(name);
expect(scoped.jobs['windows-scoped-tests'].if).toBe('always()');
});
});
describe('Windows Setup E2E runs by install path', () => {
const paths: string[] = setupE2e.on.pull_request.paths;
test('pull requests on install paths and dispatch only; every Windows job waits on windows-scope', () => {
expect(Object.keys(setupE2e.on).sort()).toEqual(['pull_request', 'workflow_dispatch']);
expect(setupE2e.on.pull_request.branches).toEqual(['main']);
for (const p of ['setup', 'setup.ps1', 'package.json', 'bun.lock', 'gstack-upgrade/**', 'bin/gstack-config', 'bin/gstack-patch-names',
'bin/gstack-state-root.sh', 'bin/gstack-migrate-claude-code', 'hosts/claude/hooks/**', 'docs/troubleshooting.md', `.github/workflows/${SETUP_FILE}`]) expect(paths).toContain(p);
expect(new Set(paths).size).toBe(paths.length);
for (const p of paths.filter(p => !p.includes('*'))) expect([p, fs.existsSync(path.join(ROOT, p))]).toEqual([p, true]);
const scope = setupE2e.jobs['windows-scope'];
expect(scope['runs-on']).toBe('ubuntu-24.04');
expect(scope.steps[0].with).toEqual({ 'fetch-depth': 2, 'persist-credentials': false });
const windowsJobs = Object.keys(setupE2e.jobs).filter(name => name !== 'windows-scope');
expect(windowsJobs).toEqual(['windows-setup', 'held-install-journey']);
for (const name of windowsJobs) {
expect([name, setupE2e.jobs[name]['runs-on'], setupE2e.jobs[name].needs, setupE2e.jobs[name].if])
.toEqual([name, 'windows-latest', 'windows-scope', "needs.windows-scope.outputs.install == 'true'"]);
}
});
test("windows-scope's install list is the paths list", () => {
const list = setupScopeScript.slice(setupScopeScript.indexOf("<<'PATHS'\n") + 10, setupScopeScript.indexOf('\nPATHS\n')).split('\n');
expect(list).toEqual(paths);
});
test('pregate still reads windows-setup as a Windows lane obligation (no condition keys on a dispatch input)', () => {
const lanes = parseWorkflow(SETUP_FILE, setupText, 'linux').filter(l => l.runsTests);
expect(lanes.map(l => [l.job, l.manual, l.tier])).toContainEqual(['windows-setup', false, 'platform']);
});
test.skipIf(!CAN_RUN)('a version-only package.json change starts no Windows job; any other install change does', () => {
const scope = (base: Record<string, string>, change: Record<string, string>) => {
const dir = repo(base, change);
try {
const r = runStep(setupScopeScript, dir);
expect(r.status, r.stderr).toBe(0);
return r.outputs.install;
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
};
const base = { 'package.json': PKG('1.0.0'), setup: '#!/bin/bash\n', 'lib/x.ts': 'export const x = 1;\n', 'hosts/claude/hooks/a/b.ts': 'a\n' };
expect(scope(base, { 'package.json': PKG('1.0.1'), 'lib/x.ts': 'export const x = 2;\n' })).toBe('false');
expect(scope(base, { 'package.json': PKG('1.0.1', '"a": "1.0.1"') })).toBe('true');
expect(scope(base, { 'package.json': PKG('1.0.1'), setup: '#!/bin/bash\necho changed\n' })).toBe('true');
expect(scope(base, { 'hosts/claude/hooks/a/b.ts': 'b\n' })).toBe('true');
expect(scope({ 'package.json': PKG('1.0.0', '"version": "1.0.0"') }, { 'package.json': PKG('1.0.0', '"version": "1.0.1"') })).toBe('true');
const dispatched = runStep(setupScopeScript, ROOT, { GITHUB_EVENT_NAME: 'workflow_dispatch' });
expect([dispatched.status, dispatched.outputs.install]).toEqual([0, 'true']);
});
});
describe('the path-scoped Windows shard on pull requests', () => {
test('every pull request runs the cheap scope job; a dispatch names its files; read-only and secretless', () => {
expect(Object.keys(scoped.on).sort()).toEqual(['pull_request', 'workflow_dispatch']);
expect(scoped.on.pull_request).toEqual({ branches: ['main'] });
expect(scoped.on.workflow_dispatch.inputs.files).toMatchObject({ type: 'string', required: true });
expect(scoped.permissions).toEqual({ contents: 'read' });
expect(scopedText).not.toContain('secrets.');
expect(scopedText).not.toContain('pull_request_target');
expect(scoped.concurrency.group).toBe('windows-scoped-tests-${{ github.event.pull_request.number || github.run_id }}');
const scope = scoped.jobs['windows-scope'];
expect(scope['runs-on']).toBe('ubuntu-24.04');
expect(scope.steps[0].with).toEqual({ 'fetch-depth': 2, 'persist-credentials': false });
const shard = scoped.jobs['windows-scoped-shard'];
expect(shard['runs-on']).toBe('windows-latest');
expect(shard.needs).toBe('windows-scope');
expect(shard.steps[0].with).toEqual({ 'persist-credentials': false });
const run = shard.steps.find((s: any) => s.name === 'Run the scoped Windows-safe test files');
expect(run.env.FILES).toBe('${{ inputs.files || needs.windows-scope.outputs.files }}');
expect(run.env.GSTACK_FREE_RETRY_FLAKY).toBe('1');
expect(run.run).toContain('bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-scoped-files.txt"');
expect(scoped.jobs['windows-scoped-tests'].needs).toEqual(['windows-scope', 'windows-scoped-shard']);
});
test('pregate reads the scoped shard as manual, so it adds no remote-clear obligation', () => {
const lanes = parseWorkflow(SCOPED_FILE, scopedText, 'linux').filter(l => l.runsTests);
expect(lanes.map(l => [l.job, l.manual])).toEqual([['windows-scoped-shard', true]]);
});
test("#3758's and #3829's failing test files are in the curated Windows-safe subset", () => {
const { safe } = curateWindowsSafe(collectFreeTestFiles(ROOT), ROOT);
expect(safe).toContain('test/hook-scripts.test.ts');
expect(safe).toContain('test/review-risk-linear.test.ts');
});
const CAN_SELECT = CAN_RUN && !!Bun.which('jq');
/** Filter, then select with a stub bun whose curated plan holds the given files. */
function scopeJob(dir: string, curated: string[]) {
const filter = runStep(filterScript, dir);
expect(filter.status, filter.stderr).toBe(0);
const scope = fs.readFileSync(path.join(filter.temp, 'windows-scope'), 'utf8');
if (filter.outputs.windows !== 'true') return { windows: filter.outputs.windows, scope, files: undefined as string | undefined };
const bin = path.join(filter.temp, 'bin');
fs.mkdirSync(bin);
fs.writeFileSync(path.join(bin, 'bun'), '#!/bin/sh\nwhile [ $# -gt 0 ]; do [ "$1" = --ci-plan ] && printf \'%s\' "$PLAN_JSON" > "$2"; shift; done\necho \'{"shard":[1]}\'\n', { mode: 0o755 });
const plan = JSON.stringify({ version: 1, revision: 'r', shards: [{ shard: 1, predictedMs: 1, files: curated }], id: 'x' });
const select = runStep(selectScript, dir, { PATH: `${toShell(bin)}:${process.env.PATH}`, PLAN_JSON: plan }, filter.temp);
expect(select.status, select.stderr).toBe(0);
return { windows: filter.outputs.windows, scope, files: select.outputs.files };
}
const CURATED = ['test/hook-scripts.test.ts', 'test/review-risk-linear.test.ts', 'test/review-risk-routing.test.ts'];
test.skipIf(!CAN_SELECT)('the select step reads the real curated plan: a changed Windows-safe test is picked, a POSIX-only one is not', () => {
const temp = newTemp();
fs.writeFileSync(path.join(temp, 'changed-files'), 'freeze/bin/check-freeze.sh\ntest/hook-scripts.test.ts\ntest/windows-path-scope-workflow.test.ts\n');
const select = runStep(selectScript, ROOT, { PATH: `${toShell(path.dirname(process.execPath))}:${process.env.PATH}` }, temp);
expect(select.status, select.stderr).toBe(0);
expect(select.outputs.files).toBe('test/hook-scripts.test.ts');
}, 60_000);
test.skipIf(!CAN_SELECT)('#3758: the hook paths put it in scope and the Windows job runs test/hook-scripts.test.ts', () => {
const dir = prRepo(PR_3758);
try {
const job = scopeJob(dir, CURATED);
expect(job.windows).toBe('true');
expect(job.scope).toContain('path: careful/bin/hook-extract.sh');
expect(job.scope).toContain('path: freeze/bin/check-freeze.sh');
expect(job.files).toBe('test/hook-scripts.test.ts');
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test.skipIf(!CAN_SELECT)('#3829: no path names it; the spawn line it added puts it in scope and the job runs its changed tests', () => {
const dir = prRepo(PR_3829, { 'test/review-risk-linear.test.ts': LINE_3829 });
try {
const job = scopeJob(dir, CURATED);
expect(job.windows).toBe('true');
expect(job.scope.trim().split('\n')).toEqual([`content: test/review-risk-linear.test.ts: +${LINE_3829}`]);
expect(job.files).toBe('test/review-risk-linear.test.ts\ntest/review-risk-routing.test.ts');
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test.skipIf(!CAN_SELECT)('negative controls: a docs-and-stamp change and a pure-logic change start no Windows job', () => {
for (const [files, lines] of [
[['CHANGELOG.md', 'VERSION', 'package.json', 'docs/support-tiers.md', 'changelog.d/x.md'], {}],
[['lib/review-depth.ts', 'test/review-depth.test.ts'], { 'lib/review-depth.ts': 'export const LIMIT = 3;', 'test/review-depth.test.ts': ' expect(LIMIT).toBe(3);' }],
] as Array<[string[], Record<string, string>]>) {
const dir = prRepo(files, lines);
try {
const job = scopeJob(dir, [...CURATED, 'test/review-depth.test.ts']);
expect([files, job.windows, job.scope]).toEqual([files, 'false', '']);
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
}
});
test.skipIf(!CAN_SELECT)('in scope but no changed Windows-safe test file: no files, so no Windows job', () => {
const dir = prRepo(['bin/gstack-example', 'test/posix-only.test.ts']);
try {
const job = scopeJob(dir, CURATED);
expect([job.windows, job.files]).toEqual(['true', undefined]);
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test.skipIf(!Bun.which('bash'))('the aggregate passes when nothing is in scope and otherwise requires the Windows job', () => {
const aggregate = scoped.jobs['windows-scoped-tests'].steps[0];
expect(aggregate.env).toEqual({ SCOPE_RESULT: '${{ needs.windows-scope.result }}', SHARD_RESULT: '${{ needs.windows-scoped-shard.result }}', FILES: '${{ inputs.files || needs.windows-scope.outputs.files }}' });
const status = (env: Record<string, string>) => runStep(aggregate.run, ROOT, env).status;
expect(status({ SCOPE_RESULT: 'success', SHARD_RESULT: 'skipped', FILES: '' })).toBe(0);
expect(status({ SCOPE_RESULT: 'success', SHARD_RESULT: 'success', FILES: 'test/a.test.ts' })).toBe(0);
expect(status({ SCOPE_RESULT: 'success', SHARD_RESULT: 'failure', FILES: 'test/a.test.ts' })).not.toBe(0);
expect(status({ SCOPE_RESULT: 'success', SHARD_RESULT: 'cancelled', FILES: 'test/a.test.ts' })).not.toBe(0);
expect(status({ SCOPE_RESULT: 'failure', SHARD_RESULT: 'skipped', FILES: '' })).not.toBe(0);
});
});