fix(ship-queue): a PREGATE_ENVIRONMENT local record is final; no mirror stands in for it

Merging origin/main b004c06 (#3480) into this branch created a semantic
conflict between two individually correct parents: #3480 made
verifyPregate return PREGATE_ENVIRONMENT (instead of PREGATE_INCOMPLETE)
when every incomplete check was caused by the environment, and this
branch's by-tree mirror reader in ready() treated only PREGATE_LANE_FAILED
and PREGATE_INCOMPLETE as final. Merged, a local record whose secrets scan
never finished fell through to a same-tree tests-only mirror and ready
passed without a secrets scan, against the rule this branch documents
("a record whose own checks failed or stayed incomplete is never
overridden").

ready() now keeps a PREGATE_ENVIRONMENT record final as well.

Regression in test/gstack-ship-queue.test.ts ("one READY block ..."): an
environment-incomplete secrets record beside a passing same-tree mirror
is refused with (PREGATE_ENVIRONMENT) and no "from the state root"
suffix. Red on the bare merge 28b64d7 (expected exit 3, received 0),
green with this fix.

Found by the pass-4 delta review of the merge.

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 20:47:46 +00:00
1 parent 28b64d7115
commit 020bdefb41
2 files changed
+10 -3

No files matched your search

+3 -2
View File
@@ -231,14 +231,15 @@ export function ready(ctx: QueueContext, o: { pr: string; pregate?: string; gate
let pregateOk = false;
const treeNow = wtreeFingerprint({ cwd: ctx.repoRoot });
// The mirror stands in for a record that is missing, unreadable, recorded for another tree, or current but
// waiting on a remote receipt another checkout holds; a record whose own checks failed or stayed incomplete is final.
// waiting on a remote receipt another checkout holds; a record whose own checks failed or stayed incomplete (the diff's doing
// or the environment's) is final.
let useMirror = true;
if (fs.existsSync(pregateFile)) {
const read = readPregateFile(pregateFile);
if (read.file) {
const v = verifyPregate(ctx.repoRoot, read.file, treeNow);
pregateOk = v.ok;
useMirror = !v.ok && v.code !== 'PREGATE_LANE_FAILED' && v.code !== 'PREGATE_INCOMPLETE';
useMirror = !v.ok && v.code !== 'PREGATE_LANE_FAILED' && v.code !== 'PREGATE_INCOMPLETE' && v.code !== 'PREGATE_ENVIRONMENT';
pregateLine = `PREGATE: ${read.file.verdict} tree=${read.file.tree.slice(0, 12)} head=${(read.file.head ?? '').slice(0, 12)} checks=${read.file.checks.length}${v.ok ? '' : ` — ${v.message} (${v.code})`}`;
} else pregateLine = `PREGATE: unreadable (${read.errors[0]?.message ?? 'invalid'})`;
}
+7 -1
View File
@@ -329,7 +329,8 @@ describe('ready', () => {
expect([current.status, current.err]).toEqual([EXIT.ok, '']);
expect(line(current.out, 'PREGATE:')).toBe(`PREGATE: pass tree=${movedTree.slice(0, 12)} head=${moved.slice(0, 12)} checks=1`);
// Value: protects=a same-tree record that only waits on a remote receipt (the small-machine run) is cleared by the clone's mirror, an
// unreadable record falls through to the mirror, and a record whose own checks failed or stayed incomplete is never overridden by a green mirror;
// unreadable record falls through to the mirror, and a record whose own checks failed or stayed incomplete is never overridden by a green mirror,
// including an incomplete check the environment caused (PREGATE_ENVIRONMENT, #3480: a secrets scanner that never finished is not a scanned diff);
// fails_when=ready reads the mirror only for another tree (the documented lane case refuses PREGATE_REMOTE_UNCLEARED) or lets a mirror hide a red
// or incomplete local run; why_new=the rows above cover only stale, missing and current records; seam=none; no_claim=the mirror's writer
const local = path.join(repo.root, 'pregate-local.json');
@@ -350,6 +351,11 @@ describe('ready', () => {
const incompleteLocal = queue(repo, ['ready', '--pr', '1', '--pregate', local], { ...movedShim, GSTACK_STATE_ROOT: stateRoot });
expect(incompleteLocal.status).toBe(EXIT.refused);
expect(line(incompleteLocal.out, 'PREGATE:')).toMatch(/\(PREGATE_INCOMPLETE\)$/);
fs.writeFileSync(local, JSON.stringify({ schema_version: 1, tree: movedTree, head: moved, verdict: 'incomplete', checks: [{ id: 'secrets', status: 'incomplete', cause: 'environment', environment: { kind: 'missing-binary', evidence: 'gstack-redact: not on PATH', proof: 'launch' } }] }));
const environmentLocal = queue(repo, ['ready', '--pr', '1', '--pregate', local], { ...movedShim, GSTACK_STATE_ROOT: stateRoot });
expect(environmentLocal.status).toBe(EXIT.refused);
expect(line(environmentLocal.out, 'PREGATE:')).toMatch(/\(PREGATE_ENVIRONMENT\)$/);
expect(environmentLocal.out).not.toContain(suffix);
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);