From 1a277804436fcd9d89d4a3bea781fbccbd4e33f8 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 18 May 2026 22:41:57 -0400 Subject: [PATCH] feat(sdk): empty phase dir surfaces NO_UAT_FILES reason (#3184) Cycle 4 of ~15: phase dir present but no *-HUMAN-UAT.md files now returns a typed NO_UAT_FILES reason instead of an empty reasons array (which prior cycles used as a 'shouldn't happen' fallback). Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/src/query/phase-uat-passed.test.ts | 18 ++++++++++++++++++ sdk/src/query/phase-uat-passed.ts | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/sdk/src/query/phase-uat-passed.test.ts b/sdk/src/query/phase-uat-passed.test.ts index 71b4ba4f7..6f2179ade 100644 --- a/sdk/src/query/phase-uat-passed.test.ts +++ b/sdk/src/query/phase-uat-passed.test.ts @@ -43,6 +43,24 @@ describe('isPhaseUatPassed', () => { expect(result.reasons.length).toBe(0); }); + it('returns passed=false with NO_UAT_FILES reason when phase dir has no UAT files', async () => { + const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c4-')); + try { + const phaseDir = join(localTmp, '.planning', 'phases', '05-empty'); + await mkdir(phaseDir, { recursive: true }); + // Write a non-UAT file to ensure the dir exists but has no *-HUMAN-UAT.md + await writeFile(join(phaseDir, '05-PLAN.md'), '# Plan\nNothing here.\n'); + + const result = await isPhaseUatPassed(localTmp, '5'); + expect(result.passed).toBe(false); + expect(result.items.length).toBe(0); + expect(result.reasons.length).toBe(1); + expect(result.reasons[0].code).toBe(REASON_CODE.NO_UAT_FILES); + } finally { + await rm(localTmp, { recursive: true, force: true }); + } + }); + it('returns passed=false with NO_PHASE_DIR reason when phase has no directory', async () => { const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c3-')); try { diff --git a/sdk/src/query/phase-uat-passed.ts b/sdk/src/query/phase-uat-passed.ts index 1fd90c234..7403a0aeb 100644 --- a/sdk/src/query/phase-uat-passed.ts +++ b/sdk/src/query/phase-uat-passed.ts @@ -12,6 +12,7 @@ import { resolvePhaseDir } from './phase-list-queries.js'; export const REASON_CODE = Object.freeze({ NON_PASS_RESULT: 'non_pass_result', NO_PHASE_DIR: 'no_phase_dir', + NO_UAT_FILES: 'no_uat_files', } as const); export type ReasonCode = typeof REASON_CODE[keyof typeof REASON_CODE]; @@ -74,6 +75,15 @@ export async function isPhaseUatPassed( const files = await readdir(dir); const uatFiles = files.filter((f) => f.endsWith('-HUMAN-UAT.md')); + if (uatFiles.length === 0) { + return { + passed: false, + reasons: [{ code: REASON_CODE.NO_UAT_FILES }], + reasonsHuman: [], + items: [], + }; + } + const items: UatItem[] = []; const reasons: UatReason[] = [];