From 9d7f7aba8153637fa0203daf8e33980f098abfdb Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 17:09:12 +0000 Subject: [PATCH] test: base-red coverage pass 2 (write --base-red, summary line, classify usage before fetch, unresolved-head detail) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/base-red.test.ts | 55 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/test/base-red.test.ts b/test/base-red.test.ts index dc18cf407..c2f868b8d 100644 --- a/test/base-red.test.ts +++ b/test/base-red.test.ts @@ -14,7 +14,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { classifyBaseRed, parseEvidence, pausePath, readPause, receiptBaseRed, shellRunner, waitBaseGreen, writePause, type FailureEvidence, type Runner } from '../lib/base-red'; import { EXIT } from '../lib/headless-artifacts'; -import { bindReceipt, extractReceipt, placeReceiptFirst } from '../lib/ship-receipt'; +import { bindReceipt, extractReceipt, placeReceiptFirst, receiptSummaryLine } from '../lib/ship-receipt'; import { BASE_RED_KEY_VERSION, validatePolicy, type BaseRedPolicy } from '../lib/ship-policy'; const ROOT = path.resolve(import.meta.dir, '..'); @@ -210,6 +210,7 @@ describe('coverage audit additions', () => { const a = counting(); const r = classifyBaseRed({ repoRoot: work, base: 'main', head: 'no-such-ref', evidence: evidence(), policy: POLICY, runner: a.runner }); expect([r.code, r.cases.every(c => c.verdict === 'IN_BRANCH'), a.calls]).toEqual(['BASE_RED_REPRO_ERROR', true, []]); + expect([r.detail, r.cases[0]!.reason]).toEqual([`cannot resolve origin/main or no-such-ref`, 'unresolved commit']); }); // Generated by /ship Step 7 coverage audit (PR #3611). @@ -228,4 +229,56 @@ describe('coverage audit additions', () => { expect(w.status).toBe(EXIT.refused); expect(w.stderr).toContain('BASE_RED_UNCONFIGURED'); }); + + // Generated by /ship Step 7 coverage audit (PR #3611), pass 2. + // Value: protects=write --base-red adds the base_red block from a valid pause, and an unreadable or non-pause file exits usage with nothing written; + // fails_when=the flag is ignored, so an unconditional receipt is written for a paused run; why_new=no test runs write with --base-red; + // seam=none; no_claim=PR body upload via gh (local body file only) + test('write --base-red makes the receipt conditional, and a bad pause file writes nothing', () => { + fixture(); + const ev = evidence(); + const { file, record } = writePause(work, 'feat', classifyBaseRed({ repoRoot: work, base: 'main', head: 'HEAD', evidence: ev, policy: POLICY }), ev); + const body = path.join(root, 'body.md'); + const run = (pause: string) => spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-ship-receipt'), 'write', '--pr', '7', '--base', 'main', '--session-kind', 'x', '--body', body, '--base-red', pause], { cwd: work, encoding: 'utf8', timeout: 120_000, env: { ...process.env, ...ID, GSTACK_STATE_ROOT: state } }); + for (const bad of [path.join(root, 'missing.json'), path.join(root, 'ev.json')]) { + fs.writeFileSync(path.join(root, 'ev.json'), JSON.stringify(ev)); + fs.writeFileSync(body, 'prose\n'); + const r = run(bad); + expect([r.status, r.stderr.includes('--base-red: '), r.stderr.includes('nothing written'), fs.readFileSync(body, 'utf8')]).toEqual([EXIT.usage, true, true, 'prose\n']); + } + const ok = run(file); + expect(ok.status).toBe(EXIT.ok); + const head = git('rev-parse', 'HEAD'); + const written = fs.readFileSync(body, 'utf8'); + expect(extractReceipt(written).receipt?.base_red).toEqual(receiptBaseRed(record)); + const bound = bindReceipt(written, '7', [head]); + expect(bound.ok ? 'bound' : bound.code).toBe('QUEUE_RECEIPT_CONDITIONAL'); + }); + + // Generated by /ship Step 7 coverage audit (PR #3611), pass 2. + // Value: protects=the SHIP_RECEIPT summary line names base_red= for a conditional receipt and omits it otherwise; fails_when=the base_red part is dropped or printed unconditionally; + // why_new=summary-line tests cover carried_from only; seam=none; no_claim=none beyond protects + test('the summary line names the red base only for a conditional receipt', () => { + const base = 'c'.repeat(40); + const plain = { schema_version: 1, head: 'a'.repeat(40), base: 'main' } as any; + expect(receiptSummaryLine({ ...plain, base_red: { cases: ['x'], files: ['t/x.sh'], base, lane: 'tests', expires_at: '2026-10-30T00:00:00Z' } })).toContain(` base_red=${base.slice(0, 12)}`); + expect(receiptSummaryLine(plain)).not.toContain('base_red='); + }); + + // Generated by /ship Step 7 coverage audit (PR #3611), pass 2. + // Value: protects=classify exits usage on a missing --failures, an unreadable file or invalid evidence before any git fetch; fails_when=evidence checks move after the policy fetch or stop refusing; + // why_new=parseEvidence is unit-tested only, never through the CLI; seam=none; no_claim=none beyond protects (GIT_TRACE proves no fetch ran) + test('classify refuses bad evidence with a usage exit before fetching anything', () => { + git('commit', '-q', '--allow-empty', '-m', 'init'); + const trace = path.join(root, 'git-trace.log'); + const bad = path.join(root, 'bad.json'); + fs.writeFileSync(bad, JSON.stringify({ lane: 'tests', total: 1, failures: [{ id: 'x', file: '/abs.sh', signature: 's' }] })); + const cases: Array<[string[], string]> = [[[], '--failures is required'], [['--failures', path.join(root, 'missing.json')], 'cannot read '], [['--failures', bad], 'must be repo-relative']]; + for (const [args, why] of cases) { + const r = spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-base-red'), 'classify', ...args], { cwd: work, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...ID, GSTACK_STATE_ROOT: state, GIT_TRACE: trace } }); + expect([r.status, r.stderr.includes(why)]).toEqual([EXIT.usage, true]); + } + const traced = fs.readFileSync(trace, 'utf8'); + expect([traced.includes('rev-parse'), traced.includes('fetch')]).toEqual([true, false]); + }); });