feat(sdk): merge frontmatter human_verification items into roster (#3184)
Cycle 11 of ~15: reuses parseVerificationFrontmatterItems from uat.ts. Frontmatter-declared manual-verification items roll into the same items[] roster and emit HUMAN_VERIFICATION_NEEDED reasons so passed=false is justified, not silent.
This commit is contained in:
@@ -273,6 +273,39 @@ expected: thing
|
||||
}
|
||||
});
|
||||
|
||||
it("human_verification items in frontmatter contribute HUMAN_VERIFICATION_NEEDED reasons", async () => {
|
||||
const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c11-'));
|
||||
try {
|
||||
const phaseDir = join(localTmp, '.planning', 'phases', '05-human-verification');
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
const content = `---
|
||||
status: complete
|
||||
phase: 5
|
||||
source: roadmap
|
||||
started: 2026-05-18T00:00:00Z
|
||||
updated: 2026-05-18T00:00:00Z
|
||||
human_verification:
|
||||
- name: manual smoke test
|
||||
expected: app loads
|
||||
---
|
||||
|
||||
### 1. Real pass
|
||||
expected: thing
|
||||
result: pass
|
||||
`;
|
||||
await writeFile(join(phaseDir, '05-HUMAN-UAT.md'), content);
|
||||
|
||||
const result = await isPhaseUatPassed(localTmp, '5');
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.items.length).toBe(2);
|
||||
expect(result.reasons.length).toBe(1);
|
||||
expect(result.reasons[0].code).toBe(REASON_CODE.HUMAN_VERIFICATION_NEEDED);
|
||||
expect(result.reasons[0].itemName).toBe('manual smoke test');
|
||||
} finally {
|
||||
await rm(localTmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("emits CASE_MISMATCH reason when result value is \"PASS\" (uppercase variant of pass)", async () => {
|
||||
const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c10-'));
|
||||
try {
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join, relative } from 'node:path';
|
||||
import { resolvePhaseDir } from './phase-list-queries.js';
|
||||
import { extractFrontmatter } from './frontmatter.js';
|
||||
import { parseVerificationFrontmatterItems } from './uat.js';
|
||||
|
||||
export const REASON_CODE = Object.freeze({
|
||||
NON_PASS_RESULT: 'non_pass_result',
|
||||
CASE_MISMATCH: 'case_mismatch',
|
||||
HUMAN_VERIFICATION_NEEDED: 'human_verification_needed',
|
||||
NO_PHASE_DIR: 'no_phase_dir',
|
||||
NO_UAT_FILES: 'no_uat_files',
|
||||
} as const);
|
||||
@@ -127,6 +130,21 @@ export async function isPhaseUatPassed(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Merge frontmatter human_verification items into the roster.
|
||||
const fm = extractFrontmatter(content);
|
||||
const fmItems = parseVerificationFrontmatterItems(fm);
|
||||
for (const fmItem of fmItems) {
|
||||
const name = String(fmItem.name ?? '');
|
||||
// Add a synthetic UatItem so items.length is accurate.
|
||||
items.push({ test: -1, name, expected: String(fmItem.expected ?? ''), result: 'human_needed' });
|
||||
reasons.push({
|
||||
code: REASON_CODE.HUMAN_VERIFICATION_NEEDED,
|
||||
file: relFile,
|
||||
itemName: name,
|
||||
capturedValue: 'human_needed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const passed = items.length > 0 && reasons.length === 0;
|
||||
|
||||
@@ -195,7 +195,7 @@ function parseUatItems(content: string): Record<string, unknown>[] {
|
||||
* rather than the body, parseVerificationItems was returning [] because it
|
||||
* only searched the body for a "## Human Verification" heading.
|
||||
*/
|
||||
function parseVerificationFrontmatterItems(fm: Record<string, unknown>): Record<string, unknown>[] {
|
||||
export function parseVerificationFrontmatterItems(fm: Record<string, unknown>): Record<string, unknown>[] {
|
||||
const items: Record<string, unknown>[] = [];
|
||||
const hvArray = fm.human_verification;
|
||||
if (!Array.isArray(hvArray)) return items;
|
||||
|
||||
Reference in New Issue
Block a user