diff --git a/src/resources/extensions/gsd/auto-dispatch.ts b/src/resources/extensions/gsd/auto-dispatch.ts index 91918938f..b537d8916 100644 --- a/src/resources/extensions/gsd/auto-dispatch.ts +++ b/src/resources/extensions/gsd/auto-dispatch.ts @@ -129,6 +129,21 @@ export function setRewriteCount(basePath: string, count: number): void { writeFileSync(filePath, JSON.stringify({ count, updatedAt: new Date().toISOString() }) + "\n"); } +// ─── Helpers ───────────────────────────────────────────────────────────── + +/** + * Returns true when the verification_operational value indicates that no + * operational verification is needed. Covers common phrasings the planning + * agent may use: "None", "None required", "N/A", "Not applicable", etc. + * + * @see https://github.com/gsd-build/gsd-2/issues/2931 + */ +export function isVerificationNotApplicable(value: string): boolean { + const v = (value ?? "").toLowerCase().trim(); + if (!v || v === "none") return true; + return /^(?:none[\s._-]*(?:required|needed|planned)?|n\/?a|not[\s._-]+(?:applicable|required|needed)|no[\s._-]+operational[\s\S]*)$/i.test(v); +} + // ─── Rules ──────────────────────────────────────────────────────────────── export const DISPATCH_RULES: DispatchRule[] = [ @@ -672,7 +687,7 @@ export const DISPATCH_RULES: DispatchRule[] = [ if (isDbAvailable()) { const milestone = getMilestone(mid); if (milestone?.verification_operational && - milestone.verification_operational.toLowerCase() !== "none") { + !isVerificationNotApplicable(milestone.verification_operational)) { const validationPath = resolveMilestoneFile(basePath, mid, "VALIDATION"); if (validationPath) { const validationContent = await loadFile(validationPath); diff --git a/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts b/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts new file mode 100644 index 000000000..a9ae8d83a --- /dev/null +++ b/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts @@ -0,0 +1,82 @@ +/** + * Regression test for #2931: completing-milestone gate should treat + * "None required", "N/A", "Not applicable", etc. as equivalent to "none" + * and skip the operational verification content check entirely. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { isVerificationNotApplicable } from "../auto-dispatch.ts"; + +test("isVerificationNotApplicable: bare 'none' is not applicable", () => { + assert.equal(isVerificationNotApplicable("none"), true); +}); + +test("isVerificationNotApplicable: 'None' (capitalized) is not applicable", () => { + assert.equal(isVerificationNotApplicable("None"), true); +}); + +test("isVerificationNotApplicable: 'NONE' (uppercase) is not applicable", () => { + assert.equal(isVerificationNotApplicable("NONE"), true); +}); + +test("isVerificationNotApplicable: 'None required' is not applicable (#2931)", () => { + assert.equal(isVerificationNotApplicable("None required"), true); +}); + +test("isVerificationNotApplicable: 'None needed' is not applicable", () => { + assert.equal(isVerificationNotApplicable("None needed"), true); +}); + +test("isVerificationNotApplicable: 'None planned' is not applicable", () => { + assert.equal(isVerificationNotApplicable("None planned"), true); +}); + +test("isVerificationNotApplicable: 'N/A' is not applicable", () => { + assert.equal(isVerificationNotApplicable("N/A"), true); +}); + +test("isVerificationNotApplicable: 'n/a' is not applicable", () => { + assert.equal(isVerificationNotApplicable("n/a"), true); +}); + +test("isVerificationNotApplicable: 'Not applicable' is not applicable", () => { + assert.equal(isVerificationNotApplicable("Not applicable"), true); +}); + +test("isVerificationNotApplicable: 'Not required' is not applicable", () => { + assert.equal(isVerificationNotApplicable("Not required"), true); +}); + +test("isVerificationNotApplicable: 'Not needed' is not applicable", () => { + assert.equal(isVerificationNotApplicable("Not needed"), true); +}); + +test("isVerificationNotApplicable: 'No operational verification needed' is not applicable", () => { + assert.equal(isVerificationNotApplicable("No operational verification needed"), true); +}); + +test("isVerificationNotApplicable: 'No operational' is not applicable", () => { + assert.equal(isVerificationNotApplicable("No operational"), true); +}); + +test("isVerificationNotApplicable: empty string is not applicable", () => { + assert.equal(isVerificationNotApplicable(""), true); +}); + +test("isVerificationNotApplicable: whitespace-only is not applicable", () => { + assert.equal(isVerificationNotApplicable(" "), true); +}); + +// Positive cases: these SHOULD require verification +test("isVerificationNotApplicable: 'Run load tests' requires verification", () => { + assert.equal(isVerificationNotApplicable("Run load tests"), false); +}); + +test("isVerificationNotApplicable: 'Verify API response times under load' requires verification", () => { + assert.equal(isVerificationNotApplicable("Verify API response times under load"), false); +}); + +test("isVerificationNotApplicable: 'Monitor error rates for 24h' requires verification", () => { + assert.equal(isVerificationNotApplicable("Monitor error rates for 24h"), false); +});