diff --git a/lib/ship-queue.ts b/lib/ship-queue.ts index f7baa44ea..e4b80b6b0 100644 --- a/lib/ship-queue.ts +++ b/lib/ship-queue.ts @@ -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'})`; } diff --git a/test/gstack-ship-queue.test.ts b/test/gstack-ship-queue.test.ts index 0aa071f76..ac411ffc1 100644 --- a/test/gstack-ship-queue.test.ts +++ b/test/gstack-ship-queue.test.ts @@ -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);