fix(gsd): stop replaying completed run-uat units (#1270)
This commit is contained in:
parent
f2b637a596
commit
ce550b2423
3 changed files with 74 additions and 31 deletions
|
|
@ -104,25 +104,6 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "run-uat (post-completion)",
|
||||
match: async ({ state, mid, basePath, prefs }) => {
|
||||
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
||||
if (!needsRunUat) return null;
|
||||
const { sliceId, uatType } = needsRunUat;
|
||||
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
||||
const uatContent = await loadFile(uatFile);
|
||||
return {
|
||||
action: "dispatch",
|
||||
unitType: "run-uat",
|
||||
unitId: `${mid}/${sliceId}`,
|
||||
prompt: await buildRunUatPrompt(
|
||||
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
||||
),
|
||||
pauseAfterDispatch: uatType !== "artifact-driven",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uat-verdict-gate (non-PASS blocks progression)",
|
||||
match: async ({ mid, basePath, prefs }) => {
|
||||
|
|
@ -152,6 +133,25 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "run-uat (post-completion)",
|
||||
match: async ({ state, mid, basePath, prefs }) => {
|
||||
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
||||
if (!needsRunUat) return null;
|
||||
const { sliceId, uatType } = needsRunUat;
|
||||
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
||||
const uatContent = await loadFile(uatFile);
|
||||
return {
|
||||
action: "dispatch",
|
||||
unitType: "run-uat",
|
||||
unitId: `${mid}/${sliceId}`,
|
||||
prompt: await buildRunUatPrompt(
|
||||
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
||||
),
|
||||
pauseAfterDispatch: uatType !== "artifact-driven",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reassess-roadmap (post-completion)",
|
||||
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
||||
|
|
|
|||
|
|
@ -530,21 +530,14 @@ export async function checkNeedsRunUat(
|
|||
const uatContent = await loadFile(uatFile);
|
||||
if (!uatContent) return null;
|
||||
|
||||
// If UAT result already exists with a PASS verdict, skip (idempotent).
|
||||
// Non-PASS verdicts (FAIL, surfaced-for-human-review) should block slice
|
||||
// progression — return the slice for re-evaluation (#1231).
|
||||
// If a UAT result already exists, the UAT unit has already run and must not
|
||||
// be re-dispatched. PASS means progression can continue; any non-PASS verdict
|
||||
// must be handled by the dispatch table's verdict gate, which stops progression
|
||||
// with a human-action message instead of replaying the same run-uat unit.
|
||||
const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
||||
if (uatResultFile) {
|
||||
const resultContent = await loadFile(uatResultFile);
|
||||
if (resultContent) {
|
||||
const verdictMatch = resultContent.match(/verdict:\s*([\w-]+)/i);
|
||||
const verdict = verdictMatch?.[1]?.toLowerCase();
|
||||
if (verdict === "pass" || verdict === "passed") return null; // PASS — skip
|
||||
// Non-PASS verdict exists — don't re-run UAT, but don't advance either.
|
||||
// Return null here since the UAT already ran; the dispatch table's
|
||||
// complete-slice rule should check the verdict before advancing.
|
||||
// For now, returning the slice signals it still needs attention.
|
||||
}
|
||||
if (resultContent) return null;
|
||||
}
|
||||
|
||||
// Classify UAT type; unknown type → treat as human-experience (human review)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
// (a)–(j) extractUatType classification (17 assertions from T01)
|
||||
// (k) run-uat prompt template loading and content integrity (8 assertions)
|
||||
// (l) dispatch precondition assertions via resolveSliceFile (4 assertions)
|
||||
// (m) stale replay guard: existing UAT-RESULT never re-dispatches (2 assertions)
|
||||
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
|
|
@ -14,6 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|||
|
||||
import { extractUatType } from '../files.ts';
|
||||
import { resolveSliceFile } from '../paths.ts';
|
||||
import { checkNeedsRunUat } from '../auto-prompts.ts';
|
||||
import { createTestContext } from './test-helpers.ts';
|
||||
|
||||
// ─── Worktree-aware prompt loader ──────────────────────────────────────────
|
||||
|
|
@ -308,6 +310,54 @@ async function main(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── (m) stale replay guard: existing UAT-RESULT never re-dispatches ─────
|
||||
console.log('\n── (m) stale replay guard');
|
||||
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
try {
|
||||
const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
|
||||
mkdirSync(roadmapDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(roadmapDir, 'M001-ROADMAP.md'),
|
||||
[
|
||||
'# M001: Test roadmap',
|
||||
'',
|
||||
'## Slices',
|
||||
'',
|
||||
'- [x] **S01: First slice** `risk:low` `depends:[]`',
|
||||
'- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
|
||||
'',
|
||||
'## Boundary Map',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: surfaced-for-human-review\n---\n');
|
||||
|
||||
const state = {
|
||||
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
||||
activeSlice: { id: 'S02', title: 'Next slice' },
|
||||
activeTask: null,
|
||||
phase: 'planning',
|
||||
recentDecisions: [],
|
||||
blockers: [],
|
||||
nextAction: 'Plan S02',
|
||||
registry: [],
|
||||
} as const;
|
||||
|
||||
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
||||
assertEq(
|
||||
result,
|
||||
null,
|
||||
'existing UAT-RESULT with non-PASS verdict does not re-dispatch run-uat; verdict gate owns blocking',
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
}
|
||||
|
||||
report();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue