Merge pull request #344 from gsd-build/copilot/fix-indefinitely-hanging-issue

This commit is contained in:
TÂCHES 2026-03-14 06:41:07 -06:00 committed by GitHub
commit f84cd3ebe3
2 changed files with 97 additions and 1 deletions

View file

@ -3065,7 +3065,11 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
}
}
// complete-slice must also produce a UAT file
// complete-slice must also produce a UAT file AND mark the slice [x] in the roadmap.
// Without the roadmap check, a crash after writing SUMMARY+UAT but before updating
// the roadmap causes an infinite skip loop: the idempotency key says "done" but the
// state machine keeps returning the same complete-slice unit (roadmap still shows
// the slice incomplete), so dispatchNextUnit recurses forever.
if (unitType === "complete-slice") {
const parts = unitId.split("/");
const mid = parts[0];
@ -3076,6 +3080,17 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
if (!existsSync(uatPath)) return false;
}
// Verify the roadmap has the slice marked [x]. If not, the completion
// record is stale — the unit must re-run to update the roadmap.
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
if (roadmapFile && existsSync(roadmapFile)) {
try {
const roadmapContent = readFileSync(roadmapFile, "utf-8");
const roadmap = parseRoadmap(roadmapContent);
const slice = roadmap.slices.find(s => s.id === sid);
if (slice && !slice.done) return false;
} catch { /* corrupt roadmap — be lenient and treat as verified */ }
}
}
}

View file

@ -409,6 +409,87 @@ function createGitBase(): string {
}
}
// ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════
// Regression for #indefinite-hang: complete-slice must verify roadmap [x] or
// the idempotency skip loops forever after a crash that wrote SUMMARY+UAT but
// did not mark the roadmap done.
const ROADMAP_INCOMPLETE = `# M001: Test Milestone
## Slices
- [ ] **S01: Test Slice** \`risk:low\`
> After this: something works
`;
const ROADMAP_COMPLETE = `# M001: Test Milestone
## Slices
- [x] **S01: Test Slice** \`risk:low\`
> After this: something works
`;
{
console.log("\n=== verifyExpectedArtifact: complete-slice — all artifacts present + roadmap marked [x] returns true ===");
const base = createFixtureBase();
try {
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8");
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8");
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8");
const result = verifyExpectedArtifact("complete-slice", "M001/S01", base);
assert(result === true, "SUMMARY + UAT + roadmap [x] should verify as true");
} finally {
cleanup(base);
}
}
{
console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY + UAT present but roadmap NOT marked [x] returns false ===");
const base = createFixtureBase();
try {
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8");
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8");
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_INCOMPLETE, "utf-8");
const result = verifyExpectedArtifact("complete-slice", "M001/S01", base);
assert(result === false, "roadmap not marked [x] should return false (crash recovery scenario)");
} finally {
cleanup(base);
}
}
{
console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY present but UAT missing returns false ===");
const base = createFixtureBase();
try {
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8");
// no UAT file
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8");
const result = verifyExpectedArtifact("complete-slice", "M001/S01", base);
assert(result === false, "missing UAT should return false");
} finally {
cleanup(base);
}
}
{
console.log("\n=== verifyExpectedArtifact: complete-slice — no roadmap file present is lenient (returns true) ===");
const base = createFixtureBase();
try {
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8");
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8");
// no roadmap file
const result = verifyExpectedArtifact("complete-slice", "M001/S01", base);
assert(result === true, "missing roadmap file should be lenient and return true");
} finally {
cleanup(base);
}
}
// ═════════════════════════════════════════════════════════════════════════════
// Results
// ═════════════════════════════════════════════════════════════════════════════