feat(sdk): throw PhaseUatPassedError on missing projectDir (#3184)
Cycle 15 of ~15: introduces PhaseUatPassedError (extends GSDError, classification Validation) with typed ERROR_CODE enum. Missing-projectDir now fails fast with a typed error rather than letting downstream code crash on a vague fs error.
This commit is contained in:
@@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { isPhaseUatPassed, REASON_CODE } from './phase-uat-passed.js';
|
||||
import { isPhaseUatPassed, REASON_CODE, PhaseUatPassedError, ERROR_CODE } from './phase-uat-passed.js';
|
||||
|
||||
const UAT_PASS_CONTENT = `---
|
||||
status: complete
|
||||
@@ -306,6 +306,18 @@ result: pass
|
||||
}
|
||||
});
|
||||
|
||||
it("throws PhaseUatPassedError with PROJECT_DIR_MISSING code when projectDir does not exist", async () => {
|
||||
const missingPath = `/nonexistent/path/that/should/never/be/real-${Date.now()}`;
|
||||
let thrown: unknown;
|
||||
try {
|
||||
await isPhaseUatPassed(missingPath, '5');
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
expect(thrown).toBeInstanceOf(PhaseUatPassedError);
|
||||
expect((thrown as PhaseUatPassedError).code).toBe(ERROR_CODE.PROJECT_DIR_MISSING);
|
||||
});
|
||||
|
||||
it("emits NO_ITEMS_EXTRACTED reason when UAT file has no parseable items, orphans, or placeholders", async () => {
|
||||
const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c14-'));
|
||||
try {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* Non-pass items (result not literally 'pass') emit a typed NON_PASS_RESULT reason.
|
||||
*/
|
||||
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { readFile, readdir, stat } 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';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
|
||||
export const REASON_CODE = Object.freeze({
|
||||
NON_PASS_RESULT: 'non_pass_result',
|
||||
@@ -24,6 +25,17 @@ export const REASON_CODE = Object.freeze({
|
||||
|
||||
export type ReasonCode = typeof REASON_CODE[keyof typeof REASON_CODE];
|
||||
|
||||
export const ERROR_CODE = Object.freeze({
|
||||
PROJECT_DIR_MISSING: 'project_dir_missing',
|
||||
} as const);
|
||||
export type ErrorCode = typeof ERROR_CODE[keyof typeof ERROR_CODE];
|
||||
|
||||
export class PhaseUatPassedError extends GSDError {
|
||||
constructor(message: string, public readonly code: ErrorCode) {
|
||||
super(message, ErrorClassification.Validation);
|
||||
}
|
||||
}
|
||||
|
||||
export type UatReason = {
|
||||
code: ReasonCode;
|
||||
file?: string;
|
||||
@@ -115,6 +127,18 @@ export async function isPhaseUatPassed(
|
||||
reasonsHuman: string[];
|
||||
items: Record<string, unknown>[];
|
||||
}> {
|
||||
try {
|
||||
await stat(projectDir);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
throw new PhaseUatPassedError(
|
||||
`projectDir does not exist: ${projectDir}`,
|
||||
ERROR_CODE.PROJECT_DIR_MISSING,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const dir = await resolvePhaseDir(phase, projectDir, workstream);
|
||||
if (!dir) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user