feat(sdk): register phase.uat-passed as canonical read-only query (#3184)
Cycle 16 of 16: wires isPhaseUatPassed into the SDK query registry as 'phase.uat-passed' (alias 'phase uat-passed'). Read-only, JSON output. Handler validates the phase argument and adapts isPhaseUatPassed to the QueryHandler signature. Adds INVALID_PHASE_NUM to ERROR_CODE for argument validation. Updates command-aliases.generated.ts to keep seam coverage parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,7 @@ export const INIT_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
|
||||
export const PHASE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
|
||||
{ canonical: 'phase.list-plans', aliases: ['phase list-plans'], subcommand: 'list-plans', mutation: false },
|
||||
{ canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], subcommand: 'list-artifacts', mutation: false },
|
||||
{ canonical: 'phase.uat-passed', aliases: ['phase uat-passed'], subcommand: 'uat-passed', mutation: false },
|
||||
{ canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], subcommand: 'next-decimal', mutation: false },
|
||||
{ canonical: 'phase.add', aliases: ['phase add'], subcommand: 'add', mutation: true },
|
||||
{ canonical: 'phase.add-batch', aliases: ['phase add-batch'], subcommand: 'add-batch', mutation: true },
|
||||
|
||||
@@ -24,6 +24,7 @@ import { verifyKeyLinks, validateConsistency, validateHealth, validateAgents, va
|
||||
import {
|
||||
phaseListPlans, phaseListArtifacts,
|
||||
} from './phase-list-queries.js';
|
||||
import { phaseUatPassed } from './phase-uat-passed.js';
|
||||
import {
|
||||
phaseAdd, phaseAddBatch, phaseInsert, phaseRemove, phaseComplete,
|
||||
phaseScaffold, phaseNextDecimal, phasesList, phasesClear, phasesArchive,
|
||||
@@ -86,6 +87,7 @@ export const FAMILY_HANDLERS: Record<string, Readonly<Record<string, QueryHandle
|
||||
phase: {
|
||||
'phase.list-plans': phaseListPlans,
|
||||
'phase.list-artifacts': phaseListArtifacts,
|
||||
'phase.uat-passed': phaseUatPassed,
|
||||
'phase.add': phaseAdd,
|
||||
'phase.add-batch': phaseAddBatch,
|
||||
'phase.insert': phaseInsert,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CommandManifestEntry } from './command-manifest.types.js';
|
||||
export const PHASE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
||||
{ family: 'phase', canonical: 'phase.list-plans', aliases: ['phase list-plans'], mutation: false, outputMode: 'json' },
|
||||
{ family: 'phase', canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], mutation: false, outputMode: 'json' },
|
||||
{ family: 'phase', canonical: 'phase.uat-passed', aliases: ['phase uat-passed'], mutation: false, outputMode: 'json' },
|
||||
{ family: 'phase', canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], mutation: false, outputMode: 'json' },
|
||||
{ family: 'phase', canonical: 'phase.add', aliases: ['phase add'], mutation: true, outputMode: 'json' },
|
||||
{ family: 'phase', canonical: 'phase.add-batch', aliases: ['phase add-batch'], mutation: true, outputMode: 'json' },
|
||||
|
||||
@@ -7,6 +7,7 @@ import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { isPhaseUatPassed, REASON_CODE, PhaseUatPassedError, ERROR_CODE } from './phase-uat-passed.js';
|
||||
import { createRegistry } from './index.js';
|
||||
|
||||
const UAT_PASS_CONTENT = `---
|
||||
status: complete
|
||||
@@ -438,3 +439,29 @@ result: PASS
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('phase.uat-passed registry wire-up (cycle 16)', () => {
|
||||
it('phase.uat-passed is registered and dispatchable through the query registry', async () => {
|
||||
const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c16-'));
|
||||
try {
|
||||
const phaseDir = join(localTmp, '.planning', 'phases', '05-walking-skeleton');
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
await writeFile(join(phaseDir, '05-HUMAN-UAT.md'), UAT_PASS_CONTENT);
|
||||
|
||||
const registry = createRegistry();
|
||||
|
||||
// The handler must be found — if not registered, this returns undefined.
|
||||
expect(registry.has('phase.uat-passed'), 'phase.uat-passed handler not found in registry').toBe(true);
|
||||
|
||||
// Dispatch via the real registry path; args[0] is the phase token.
|
||||
const result = await registry.dispatch('phase.uat-passed', ['5'], localTmp);
|
||||
|
||||
const data = result.data as { passed: boolean; items: Array<Record<string, unknown>>; reasons: unknown[] };
|
||||
expect(data.passed).toBe(true);
|
||||
expect(data.items.length).toBe(1);
|
||||
expect(data.items[0].result).toBe('pass');
|
||||
} finally {
|
||||
await rm(localTmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { resolvePhaseDir } from './phase-list-queries.js';
|
||||
import { extractFrontmatter } from './frontmatter.js';
|
||||
import { parseVerificationFrontmatterItems } from './uat.js';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
export const REASON_CODE = Object.freeze({
|
||||
NON_PASS_RESULT: 'non_pass_result',
|
||||
@@ -27,6 +28,7 @@ export type ReasonCode = typeof REASON_CODE[keyof typeof REASON_CODE];
|
||||
|
||||
export const ERROR_CODE = Object.freeze({
|
||||
PROJECT_DIR_MISSING: 'project_dir_missing',
|
||||
INVALID_PHASE_NUM: 'invalid_phase_num',
|
||||
} as const);
|
||||
export type ErrorCode = typeof ERROR_CODE[keyof typeof ERROR_CODE];
|
||||
|
||||
@@ -246,3 +248,21 @@ export async function isPhaseUatPassed(
|
||||
|
||||
return { passed, reasons, reasonsHuman: [], items };
|
||||
}
|
||||
|
||||
/**
|
||||
* QueryHandler adapter for `phase.uat-passed` registry entry.
|
||||
*
|
||||
* args[0] — phase token (required, e.g. '5' or '05-walking-skeleton').
|
||||
* Matches the args convention used by phase.list-plans / phase.list-artifacts.
|
||||
*/
|
||||
export const phaseUatPassed: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phase = args[0];
|
||||
if (typeof phase !== 'string' || phase.trim() === '') {
|
||||
throw new PhaseUatPassedError(
|
||||
'phase argument is required',
|
||||
ERROR_CODE.INVALID_PHASE_NUM,
|
||||
);
|
||||
}
|
||||
const result = await isPhaseUatPassed(projectDir, phase, workstream);
|
||||
return { data: result };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user