fix: pre-landing review fixes (ready at the stamp commit, screen on a stale merge ref, ship-run.json)

- ready accepts the pin's receipt at a slot's stamp commit (plus the slot-2
  merge of main), as verify --train already did; a real edit stays STALE
- ready --screen refuses QUEUE_SCREEN_STALE_REF when refs/pull/<n>/merge is
  missing or merges an older head; a finished screen that planned 0 cases is
  never followed, and train build seats that slot unscreened (GSTA-95 on #3431)
- gstack-ship-receipt result writes ship-run.json (a /ship record, not a run
  v1 manifest), validates --head, and prints status=incomplete on gh failures
- bindReceipt takes an optional pr and renders one-line SHIP_RECEIPT lines
- tests for every state-matrix cell the review named; the train fixture's
  merge ref is a real merge commit

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 02:07:23 +00:00
1 parent 268b0125a5
commit ec438fe8b1
17 files changed
+368 -44

No files matched your search

+37
View File
@@ -295,6 +295,43 @@ describe('ready', () => {
fs.appendFileSync(path.join(repo.work, 'pr-a.txt'), 'dirty\n');
expect(queue(repo, ['ready', '--pr', '1'], shim(moved, green)).err).toMatch(/uncommitted tracked changes: pr-a\.txt.*\(QUEUE_NOT_READY\)/);
}, 180_000);
// Value: protects=the re-READY after a slot's stamp push accepts the receipt /ship wrote for the pinned head, and only then;
// fails_when=ready binds the receipt to HEAD alone (every stamped READY turns QUEUE_RECEIPT_STALE) or accepts any ancestor's receipt;
// why_new=the READY test above binds receipts to the exact head only; seam=none; no_claim=real gh and CI (shims; unit only)
test('ready at the stamp commit accepts the receipt written for the pinned head; a real edit on top of the pin is stale', () => {
const repo = fixture();
const pin = makePr(repo, 'pr-s');
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.4.0\n');
repo.git('commit', '-qam', 'v1.2.4.0 stamp');
const stamped = repo.git('rev-parse', 'HEAD');
const shim = (prHead: string, body: string) => ghQueueShim(repo, { prs: { '1': { headRefOid: prHead, url: 'https://github.com/o/r/pull/1', body } }, checks: { [prHead]: green } });
const ok = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, stamped)], shim(stamped, shippedBody('1', pin)));
expect([ok.status, ok.err]).toEqual([EXIT.ok, '']);
expect(ok.out.split('\n')[0]).toBe(`READY #1 ${stamped} STAMP_VERSION=1.2.4.0`);
expect(ok.out).toContain(`SHIP_RECEIPT: pr=1 head=${pin.slice(0, 12)} version=- base=main`);
repo.git('checkout', '-q', 'origin/main', '-B', 'main-moves');
fs.writeFileSync(path.join(repo.work, 'other.txt'), 'another PR landed\n');
repo.git('add', 'other.txt');
repo.git('commit', '-qm', 'other PR');
repo.git('push', '-q', 'origin', 'HEAD:main');
repo.git('fetch', '-q', 'origin');
repo.git('checkout', '-q', 'pr-s');
repo.git('merge', '-q', '--no-edit', 'origin/main');
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.5.0\n');
repo.git('commit', '-qam', 'v1.2.5.0 stamp');
const slot2 = repo.git('rev-parse', 'HEAD');
const merged = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, slot2)], shim(slot2, shippedBody('1', pin)));
expect([merged.status, merged.err]).toEqual([EXIT.ok, '']);
expect(merged.out).toContain(`SHIP_RECEIPT: pr=1 head=${pin.slice(0, 12)} version=- base=main`);
repo.git('reset', '-q', '--hard', stamped);
fs.appendFileSync(path.join(repo.work, 'pr-s.txt'), 'real change after /ship\n');
repo.git('commit', '-qam', 'more');
const edited = repo.git('rev-parse', 'HEAD');
const stale = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, edited)], shim(edited, shippedBody('1', pin)));
expect(stale.status).toBe(EXIT.refused);
expect(stale.err).toMatch(new RegExp(`PR #1's ship receipt is for ${pin.slice(0, 12)}, not the head ${edited.slice(0, 12)} being merged.*\\(QUEUE_RECEIPT_STALE\\)`));
}, 180_000);
});
describe('receipt digest', () => {
+94 -3
View File
@@ -6,9 +6,10 @@
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { computeCensus, extractReceipt, placeReceiptFirst, receiptSummaryLine, renderReceiptBlock } from '../lib/ship-receipt';
import { bindReceipt, computeCensus, extractReceipt, placeReceiptFirst, receiptSummaryLine, renderReceiptBlock } from '../lib/ship-receipt';
import { cleanup, ghShim, makeFixtureRepo, makePr, recordAndBundle, restamp, runBin, type FixtureRepo } from './helpers/restamp-fixture';
const repos: FixtureRepo[] = [];
@@ -165,7 +166,8 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
const dir = m![1]!;
expect(dir.startsWith(path.join(state, 'ship', 'runs'))).toBe(true);
expect(dir.endsWith(`-${head.slice(0, 12)}`)).toBe(true);
const run = JSON.parse(fs.readFileSync(path.join(dir, 'run.json'), 'utf8'));
const run = JSON.parse(fs.readFileSync(path.join(dir, 'ship-run.json'), 'utf8'));
expect(fs.existsSync(path.join(dir, 'run.json'))).toBe(false);
expect(run).toMatchObject({ schema_version: 1, skill: 'ship', status: 'complete', head, pr: 5, base: 'main', queue_mode: 'stamp-at-merge', receipt: path.join(dir, 'ship-receipt.md'), receipt_source: 'PR #5' });
expect(extractReceipt(fs.readFileSync(run.receipt, 'utf8')).receipt).toMatchObject({ pr: 5, head });
expect(ok.err.trim()).toBe(`SHIP_RECEIPT: pr=5 head=${head.slice(0, 12)} version=- base=main`);
@@ -184,11 +186,100 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
const handoff = runBin(repo, 'gstack-ship-receipt', ['result', '--body', bodyFile, '--out', path.join(repo.root, 'run-out')], env);
expect(handoff.status).toBe(EXIT.ok);
expect(handoff.out.trim()).toBe(`GSTACK_RESULT: skill=ship status=complete run=${path.join(repo.root, 'run-out')}`);
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'run-out', 'run.json'), 'utf8'))).toMatchObject({ head, pr: null, receipt_source: bodyFile, session_kind: 'unattended' });
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'run-out', 'ship-run.json'), 'utf8'))).toMatchObject({ head, pr: null, receipt_source: bodyFile, session_kind: 'unattended' });
expect(runBin(repo, 'gstack-ship-receipt', ['result', '--body', path.join(repo.root, 'absent.md')], env).status).toBe(EXIT.refused);
expect(runBin(repo, 'gstack-ship-receipt', ['result'], env).status).toBe(EXIT.usage);
expect(fs.readdirSync(path.join(state, 'ship', 'runs')).length).toBe(1);
});
// Value: protects=a --body handoff refusal names "the receipt", never "PR #-", and a gh failure is EXIT.fail, not a refusal;
// fails_when=the PR #-'s rewrite stops matching or gh errors are mapped to status=refused; why_new=the result test covers
// only the --pr refusals, an absent file and a bound handoff; seam=none; no_claim=real gh (the shim stands in; unit only)
test('a --body file with no block or a stale head is refused in the receipt\'s own words, and a gh failure is a plain failure, not a refusal', () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const env = { ...ghShim(repo, {}), GSTACK_STATE_ROOT: path.join(repo.root, 'state') };
const dir = path.join(repo.work, '.gstack/tmp');
fs.mkdirSync(dir, { recursive: true });
const noBlock = path.join(dir, 'no-block.md');
fs.writeFileSync(noBlock, 'Handoff prose without a receipt.\n');
const missing = runBin(repo, 'gstack-ship-receipt', ['result', '--body', noBlock], env);
expect(missing.status).toBe(EXIT.refused);
expect(missing.out.trim()).toBe('GSTACK_RESULT: skill=ship status=refused run=-');
expect(missing.err).toContain("the receipt's body carries no ship receipt");
expect(missing.err).not.toContain('PR #-');
const staleFile = path.join(dir, 'stale.md');
fs.writeFileSync(staleFile, renderReceiptBlock({ schema_version: 1, head: 'c'.repeat(40), base: 'main' } as any));
const stale = runBin(repo, 'gstack-ship-receipt', ['result', '--body', staleFile], env);
expect(stale.status).toBe(EXIT.refused);
expect(stale.err).toContain(`the receipt's ship receipt is for cccccccccccc, not the head ${head.slice(0, 12)} being merged`);
const invalidFile = path.join(dir, 'invalid.md');
fs.writeFileSync(invalidFile, '```gstack-ship-receipt\n{"schema_version":1}\n```\n');
const invalid = runBin(repo, 'gstack-ship-receipt', ['result', '--body', invalidFile], env);
expect([invalid.status, invalid.out.trim()]).toEqual([EXIT.refused, 'GSTACK_RESULT: skill=ship status=refused run=-']);
expect(invalid.err).toMatch(/the receipt's ship receipt is invalid \(.*\).*\(QUEUE_RECEIPT_MISSING/);
const otherPrEnv = { ...env, ...ghShim(repo, { 8: { body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 99, head, base: 'main' } as any) } }) };
const otherPr = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '8'], otherPrEnv);
expect([otherPr.status, otherPr.out.trim()]).toEqual([EXIT.refused, 'GSTACK_RESULT: skill=ship status=refused run=-']);
expect(otherPr.err).toContain("PR #8's ship receipt names pr=99, not this PR");
const pinned = path.join(dir, 'pinned.md');
fs.writeFileSync(pinned, renderReceiptBlock({ schema_version: 1, head: 'c'.repeat(40), base: 'main' } as any));
const override = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', 'c'.repeat(40), '--out', path.join(repo.root, 'pinned-out')], env);
expect([override.status, override.out.trim()]).toEqual([EXIT.ok, `GSTACK_RESULT: skill=ship status=complete run=${path.join(repo.root, 'pinned-out')}`]);
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'pinned-out', 'ship-run.json'), 'utf8')).head).toBe('c'.repeat(40));
const badHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', '../../etc'], env);
expect([badHead.status, badHead.out]).toEqual([EXIT.usage, '']);
expect(badHead.err).toContain('--head must be a hex commit id');
const noGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-receipt-nogit-'));
const outside = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned], env, noGit);
expect([outside.status, outside.err.trim()]).toEqual([EXIT.usage, 'gstack-ship-receipt result: --head <sha> is required outside a git repository']);
fs.rmSync(noGit, { recursive: true, force: true });
const ghDown = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '9'], env);
expect(ghDown.status).toBe(EXIT.fail);
expect(ghDown.err).toContain('gh pr view failed');
expect(ghDown.out.trim()).toBe('GSTACK_RESULT: skill=ship status=incomplete run=-');
expect(JSON.parse(runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '9', '--json'], env).out)).toMatchObject({ skill: 'ship', status: 'incomplete', run: '-' });
expect(fs.existsSync(path.join(repo.root, 'state', 'ship', 'runs'))).toBe(false);
});
});
describe('bindReceipt head matching (rule 22)', () => {
// Value: protects=a receipt binds only to a full or 12+ character prefix of an accepted head, and names the PR it was written for;
// fails_when=the 12-character floor is dropped (head "a" would bind any head) or the pr check is removed;
// why_new=the queue, train and result tests bind full 40-character heads only; seam=none; no_claim=gh and CLI wiring (pure function)
test('binds a full head or a 12+ character prefix, refuses shorter prefixes, other heads and another PR\'s receipt', () => {
const head = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0';
const other = 'f'.repeat(40);
const body = (h: string, pr?: number) => placeReceiptFirst('Prose.\n', { schema_version: 1, head: h, base: 'main', ...(pr === undefined ? {} : { pr }) } as any);
const rows: Array<[string, string, string[], string]> = [
['full head', head, [head], 'ok'],
['12-char prefix', head.slice(0, 12), [head], 'ok'],
['11-char prefix', head.slice(0, 11), [head], 'QUEUE_RECEIPT_STALE'],
['7-char prefix', head.slice(0, 7), [head], 'QUEUE_RECEIPT_STALE'],
['other head', other, [head], 'QUEUE_RECEIPT_STALE'],
['pin or current head', head, [other, head], 'ok'],
];
for (const [name, h, accepted, want] of rows) {
const b = bindReceipt(body(h), '3438', accepted);
expect([name, b.ok ? 'ok' : b.code]).toEqual([name, want]);
if (b.ok) expect(b.head).toBe(head);
}
const prefixBinding = bindReceipt(body(head.slice(0, 12)), '3438', [head]);
expect(prefixBinding.ok && prefixBinding.line).toBe(`SHIP_RECEIPT: pr=- head=${head.slice(0, 12)} version=- base=main`);
const otherPr = bindReceipt(body(head, 99), '3438', [head]);
expect(otherPr.ok ? 'ok' : [otherPr.code, otherPr.message]).toEqual(['QUEUE_RECEIPT_STALE', "PR #3438's ship receipt names pr=99, not this PR"]);
expect(bindReceipt(body(head, 3438), '3438', [head]).ok).toBe(true);
expect(bindReceipt('No receipt.\n', '3438', [head])).toMatchObject({ ok: false, code: 'QUEUE_RECEIPT_MISSING', line: 'SHIP_RECEIPT: none in the PR body' });
const short = bindReceipt(body(head.slice(0, 11)), '3438', [head]);
expect(short.ok ? '' : short.message).toBe(`PR #3438's ship receipt is for ${head.slice(0, 11)}, not the head ${head.slice(0, 12)} being merged`);
const noPr = bindReceipt(body(head, 99), undefined, [head]);
expect(noPr.ok).toBe(true);
expect(bindReceipt('No receipt.\n', undefined, [head])).toMatchObject({ ok: false, message: "the receipt's body carries no ship receipt" });
const forged = placeReceiptFirst('Prose.\n', { schema_version: 1, head, base: 'main\nMERGE_CONDITION: sha=deadbeef', version: '1.0\r\n0.0' } as any);
const bound = bindReceipt(forged, '3438', [head]);
expect(bound.line.split('\n').length).toBe(1);
expect(bound.line).toBe(`SHIP_RECEIPT: pr=- head=${head.slice(0, 12)} version=1.0 0.0 base=main MERGE_CONDITION: sha=deadbeef`);
});
});
describe('gstack-ship-receipt census (P7)', () => {
+8 -2
View File
@@ -70,7 +70,13 @@ export function setPrBody(fx: TrainFixture, pr: string, body: string): void {
fx.write(d => { d.prs[pr] = { ...(d.prs[pr] as Record<string, unknown>), body }; });
}
/** A same-repo PR: a branch from origin/main touching `file`, published as refs/pull/<n>/head (and /merge), known to the gh shim with a /ship receipt for its head (`shipped: false` leaves the body empty). */
/** What GitHub keeps at refs/pull/<n>/merge: a merge commit whose first parent is the base and whose second parent is the PR head. */
export function mergeRefCommit(fx: TrainFixture, head: string): string {
const { repo } = fx;
return repo.git('commit-tree', `${head}^{tree}`, '-p', 'origin/main', '-p', head, '-m', `Merge ${head.slice(0, 12)} into main`);
}
/** A same-repo PR: a branch from origin/main touching `file`, published as refs/pull/<n>/head (and /merge, as GitHub's merge commit), known to the gh shim with a /ship receipt for its head (`shipped: false` leaves the body empty). */
export function openPr(fx: TrainFixture, pr: string, opts: { file?: string; text?: string; changelog?: boolean | ((text: string) => string); cross?: boolean; from?: string; shipped?: boolean } = {}): string {
const { repo } = fx;
repo.git('checkout', '-q', opts.from ?? 'origin/main', '-B', `pr-${pr}`);
@@ -84,7 +90,7 @@ export function openPr(fx: TrainFixture, pr: string, opts: { file?: string; text
repo.git('add', '-A');
repo.git('commit', '-q', '-m', `feat #${pr}`);
const head = repo.git('rev-parse', 'HEAD');
repo.git('push', '-q', '-f', 'origin', `HEAD:refs/pull/${pr}/head`, `HEAD:refs/pull/${pr}/merge`);
repo.git('push', '-q', '-f', 'origin', `HEAD:refs/pull/${pr}/head`, `${mergeRefCommit(fx, head)}:refs/pull/${pr}/merge`);
fx.write(d => { d.prs[pr] = { headRefOid: head, isCrossRepository: !!opts.cross, state: 'OPEN', headRefName: `pr-${pr}`, url: `https://github.com/fixture/repo/pull/${pr}`, body: opts.shipped === false ? '' : shippedBody(pr, head) }; });
return head;
}
+71 -2
View File
@@ -19,7 +19,7 @@ import { TRAIN_HELP } from '../lib/ship-queue-train-cli';
import { BASE_POLICY, cleanup, runBin } from './helpers/restamp-fixture';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import {
COMPLETE, FAIL, GREEN_GATE, PASS, RED_GATE, TRAIN_BLOCK, TRAIN_POLICY, dispatches, githubOrigin, line, nonceOf, openPr, queue, readyReceipt, repoKey, setPrBody, shippedBody, trainFixture, trainJson, urlKey,
COMPLETE, FAIL, GREEN_GATE, PASS, RED_GATE, TRAIN_BLOCK, TRAIN_POLICY, dispatches, githubOrigin, line, mergeRefCommit, nonceOf, openPr, queue, readyReceipt, repoKey, setPrBody, shippedBody, trainFixture, trainJson, urlKey,
type Rule, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
@@ -177,9 +177,11 @@ describe('ready under a train policy: the screen choice', () => {
test('--screen dispatches one screen with the PR, its merge SHA and the policy; a repeat adopts it; a broad screen never adopts the narrow one', () => {
const { fx, head, pregate } = readyFixture();
const mergeSha = fx.repo.git('ls-remote', 'origin', 'refs/pull/1/merge').split(/\s/)[0]!;
expect(mergeSha).not.toBe(head);
const dry = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen', '--dry-run']);
expect(dry.status).toBe(EXIT.ok);
expect(line(dry.out, 'SCREEN_DISPATCH:')).toMatch(new RegExp(`^SCREEN_DISPATCH: evals\\.yml --ref main evals_all=false expected_sha=${head} nonce=pr\\.screen\\.narrow-\\S+ base_ref=main base_sha=[0-9a-f]{40} screen_pr=1 screen_merge_sha=${head} screen_policy=narrow \\(dry run: not dispatched\\)$`));
expect(line(dry.out, 'SCREEN_DISPATCH:')).toMatch(new RegExp(`^SCREEN_DISPATCH: evals\\.yml --ref main evals_all=false expected_sha=${mergeSha} nonce=pr\\.screen\\.narrow-\\S+ base_ref=main base_sha=[0-9a-f]{40} screen_pr=1 screen_merge_sha=${mergeSha} screen_policy=narrow \\(dry run: not dispatched\\)$`));
expect(dispatches(fx)).toEqual([]);
fx.write(d => { d.rules['evals.yml'] = { status: 'in_progress', conclusion: null }; });
const first = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
@@ -192,6 +194,52 @@ describe('ready under a train policy: the screen choice', () => {
expect(dispatches(fx).map(d => (d.inputs as any).screen_policy)).toEqual(['narrow', 'broad']);
expect(queue(fx, ['ready', '--pr', '1', '--broad']).status).toBe(EXIT.usage);
}, 120_000);
// Value: protects=--screen never dispatches against a merge ref GitHub has not recomputed for the PR head (GSTA-95 on #3431: a stale ref made evals.yml plan 0 cases and report success);
// fails_when=the merge ref's second parent is not compared with the PR head, or a missing merge ref keeps the old QUEUE_NOT_READY code; why_new=the --screen test above only sees a fresh merge ref; seam=none; no_claim=GitHub's real recompute timing (a bare origin stands in)
test('--screen refuses QUEUE_SCREEN_STALE_REF when refs/pull/<n>/merge lags the PR head or is missing, dispatching nothing; a recomputed merge ref admits it', () => {
const { fx, head, pregate } = readyFixture();
const { repo } = fx;
fs.appendFileSync(path.join(repo.work, 'pr-1.txt'), 'pushed after the merge ref was computed\n');
repo.git('commit', '-qam', 'more');
const moved = repo.git('rev-parse', 'HEAD');
repo.git('push', '-q', '-f', 'origin', 'HEAD:refs/pull/1/head');
fx.write(d => { (d.prs['1'] as any).headRefOid = moved; (d.prs['1'] as any).body = shippedBody('1', moved); d.ci[moved] = [{ status: 'completed', conclusion: 'success' }]; });
const pregate2 = path.join(repo.root, 'pregate2.json');
fs.writeFileSync(pregate2, JSON.stringify({ schema_version: 1, tree: runBin(repo, 'gstack-wtree', []).out.trim(), head: moved, verdict: 'pass', checks: [{ id: 'unit', status: 'pass' }] }));
const stale = queue(fx, ['ready', '--pr', '1', '--pregate', pregate2, '--screen']);
expect(stale.status).toBe(EXIT.refused);
expect(stale.err).toMatch(new RegExp(`refs/pull/1/merge \\([0-9a-f]{12}\\) merges ${head.slice(0, 12)}, not the PR head ${moved.slice(0, 12)}.*\\(QUEUE_SCREEN_STALE_REF\\)`));
expect(line(stale.out, 'NEXT:')).toBe('NEXT: resolve the conflict with main (or wait for GitHub to recompute refs/pull/1/merge), push, then gstack-ship-queue ready --pr 1 --screen; a screen dispatched on a stale merge ref plans no cases');
repo.git('push', '-q', 'origin', ':refs/pull/1/merge');
const missing = queue(fx, ['ready', '--pr', '1', '--pregate', pregate2, '--screen']);
expect(missing.status).toBe(EXIT.refused);
expect(missing.err).toMatch(/has no refs\/pull\/1\/merge.*\(QUEUE_SCREEN_STALE_REF\)/);
expect(dispatches(fx)).toEqual([]);
expect(fs.existsSync(path.join(fx.state, 'ship-queue', 'ready'))).toBe(false);
repo.git('push', '-q', '-f', 'origin', `${mergeRefCommit(fx, moved)}:refs/pull/1/merge`);
const fresh = queue(fx, ['ready', '--pr', '1', '--pregate', pregate2, '--screen', '--dry-run']);
expect([fresh.status, fresh.err]).toEqual([EXIT.ok, '']);
expect(line(fresh.out, 'SCREEN_DISPATCH:')).toContain('(dry run: not dispatched)');
}, 120_000);
// Value: protects=a finished screen that planned 0 cases never counts as a screen: ready dispatches a fresh one instead of adopting it and train build seats the slot unscreened with a SCREEN: line;
// fails_when=the census is not read (a hollow run is followed, or ORDER says screened); why_new=no test reads a screen run's record; seam=none; no_claim=evals.yml's real artifact (the shim's record stands in)
test('a screen run that planned 0 cases is not followed by ready and is counted as not-run by train build', () => {
const { fx, head, pregate } = readyFixture();
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', record: { verdict: 'GREEN', census: { complete: true, planned: { rule: 0, judge: 0 }, executed: { rule: 0, judge: 0 } }, panels: [] } }; });
const first = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
expect(first.status).toBe(EXIT.ok);
expect(line(first.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 100 /);
const again = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
expect(again.status).toBe(EXIT.ok);
expect(line(again.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 101 .* \(not following run 100 planned 0 cases \(concluded success\): a screen that planned nothing is not a screen\)$/);
expect(dispatches(fx, 'evals.yml').length).toBe(2);
const build = queue(fx, ['train', 'build', '--prs', `1@${head}`, '--dry-run']);
expect(build.status).toBe(EXIT.ok);
expect(line(build.out, 'SCREEN:')).toBe('SCREEN: #1 run 101 planned 0 cases (concluded success); counted as not-run, the slot rides unscreened');
expect(line(build.out, 'ORDER:')).toBe('ORDER: 1:#1(unscreened)');
}, 120_000);
});
describe('train build', () => {
@@ -353,6 +401,10 @@ describe('train build', () => {
const stale = queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`, '--dry-run']);
expect(stale.status).toBe(EXIT.refused);
expect(stale.err).toMatch(new RegExp(`PR #2's ship receipt is for aaaaaaaaaaaa, not the head ${h2.slice(0, 12)} being merged.*\\(QUEUE_RECEIPT_STALE\\)`));
setPrBody(fx, '2', '```gstack-ship-receipt\n{"schema_version":1}\n```\n');
expect(queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`, '--dry-run']).err).toMatch(/PR #2's ship receipt is invalid \(.*\).*\(QUEUE_RECEIPT_MISSING\)/);
setPrBody(fx, '2', shippedBody('1', h2));
expect(queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`, '--dry-run']).err).toMatch(/PR #2's ship receipt names pr=1, not this PR.*\(QUEUE_RECEIPT_STALE\)/);
expect(remoteRefs(fx)).toBe(refsBefore);
expect(dispatches(fx)).toEqual([]);
expect(fs.existsSync(path.join(fx.state, 'ship-queue', 'trains'))).toBe(false);
@@ -675,8 +727,25 @@ describe('verify, freeze, repair and clear', () => {
expect(trainJson(fx, nonce).merged).toEqual([]);
setPrBody(fx, '1', '');
expect(v().err).toContain('(QUEUE_RECEIPT_MISSING)');
setPrBody(fx, '1', '```gstack-ship-receipt\n{"schema_version":1}\n```\n');
const invalid = v();
expect(invalid.err).toMatch(/PR #1's ship receipt is invalid \(.*\).*\(QUEUE_RECEIPT_MISSING\)/);
expect(invalid.out).not.toContain('MERGE_CONDITION:');
setPrBody(fx, '1', shippedBody('2', h1));
const otherPr = v();
expect(otherPr.err).toMatch(/PR #1's ship receipt names pr=2, not this PR.*\(QUEUE_RECEIPT_STALE\)/);
expect(otherPr.out).not.toContain('MERGE_CONDITION:');
expect(trainJson(fx, nonce).merged).toEqual([]);
setPrBody(fx, '1', shippedBody('1', stamped));
expect(v().out).toContain(`SHIP_RECEIPT: pr=1 head=${stamped.slice(0, 12)} version=- base=main (binds the current head)`);
const saved = fx.read().prs['1'];
fx.write(d => { delete d.prs['1']; });
const unreadable = v();
expect(unreadable.status).toBe(EXIT.refused);
expect(unreadable.err).toContain('(QUEUE_PROOF_MISSING)');
expect(unreadable.out).not.toContain('MERGE_CONDITION:');
fx.write(d => { d.prs['1'] = saved; });
expect(trainJson(fx, nonce).merged).toEqual([]);
}, 300_000);
test('from slot 2 on, a head that merges main into the pin verifies when its tree is the slot\'s prefix modulo stamps', () => {