fix(doctor): fix roadmap checkbox and UAT stub immediately instead of deferring (#1819)
Remove all_tasks_done_missing_slice_uat and all_tasks_done_roadmap_not_checked from COMPLETION_TRANSITION_CODES so they are fixed at task fixLevel. Only all_tasks_done_missing_slice_summary remains deferred (requires LLM content). This closes the fragile handoff window where a session crash between last task and complete-slice left the project inconsistent. Fixes #1808 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
800cff4bc0
commit
b2fb12813f
3 changed files with 153 additions and 7 deletions
|
|
@ -76,11 +76,14 @@ export type DoctorIssueCode =
|
|||
* they are resolved by the complete-slice/complete-milestone dispatch units.
|
||||
* Consumers (e.g. auto-post-unit health tracking) should exclude these from
|
||||
* error counts when running at task fixLevel to avoid false escalation.
|
||||
*
|
||||
* Only the slice summary is deferred here because it requires LLM-generated
|
||||
* content. Roadmap checkbox and UAT stub are mechanical bookkeeping and are
|
||||
* fixed immediately to avoid inconsistent state if the session stops before
|
||||
* complete-slice runs (#1808).
|
||||
*/
|
||||
export const COMPLETION_TRANSITION_CODES = new Set<DoctorIssueCode>([
|
||||
"all_tasks_done_missing_slice_summary",
|
||||
"all_tasks_done_missing_slice_uat",
|
||||
"all_tasks_done_roadmap_not_checked",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* Regression test for #1808: Completion-transition doctor fix deferral
|
||||
* creates fragile handoff window.
|
||||
*
|
||||
* Only slice summary should be deferred (needs LLM content).
|
||||
* Roadmap checkbox and UAT stub are mechanical bookkeeping and must be
|
||||
* fixed immediately at task fixLevel to prevent inconsistent state if the
|
||||
* session stops between last task and complete-slice.
|
||||
*/
|
||||
|
||||
import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { runGSDDoctor } from "../doctor.ts";
|
||||
import { COMPLETION_TRANSITION_CODES } from "../doctor-types.ts";
|
||||
|
||||
function makeTmp(name: string): string {
|
||||
const dir = join(tmpdir(), `doctor-deferral-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal .gsd structure: milestone with one slice, one task
|
||||
* marked done with a summary — but no slice summary, no UAT, and
|
||||
* roadmap unchecked. This is the state after the last task completes.
|
||||
*/
|
||||
function buildScaffold(base: string) {
|
||||
const gsd = join(base, ".gsd");
|
||||
const m = join(gsd, "milestones", "M001");
|
||||
const s = join(m, "slices", "S01", "tasks");
|
||||
mkdirSync(s, { recursive: true });
|
||||
|
||||
writeFileSync(join(m, "M001-ROADMAP.md"), `# M001: Test
|
||||
|
||||
## Slices
|
||||
|
||||
- [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
|
||||
> Demo text
|
||||
`);
|
||||
|
||||
writeFileSync(join(m, "slices", "S01", "S01-PLAN.md"), `# S01: Test Slice
|
||||
|
||||
**Goal:** test
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] **T01: Do stuff** \`est:5m\`
|
||||
`);
|
||||
|
||||
writeFileSync(join(s, "T01-SUMMARY.md"), `---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M001
|
||||
duration: 5m
|
||||
verification_result: passed
|
||||
completed_at: 2026-01-01
|
||||
---
|
||||
|
||||
# T01: Do stuff
|
||||
|
||||
Done.
|
||||
`);
|
||||
}
|
||||
|
||||
test("COMPLETION_TRANSITION_CODES only contains slice summary code", () => {
|
||||
assert.ok(
|
||||
COMPLETION_TRANSITION_CODES.has("all_tasks_done_missing_slice_summary"),
|
||||
"summary code should still be deferred"
|
||||
);
|
||||
assert.ok(
|
||||
!COMPLETION_TRANSITION_CODES.has("all_tasks_done_missing_slice_uat"),
|
||||
"UAT code should NOT be deferred"
|
||||
);
|
||||
assert.ok(
|
||||
!COMPLETION_TRANSITION_CODES.has("all_tasks_done_roadmap_not_checked"),
|
||||
"roadmap code should NOT be deferred"
|
||||
);
|
||||
});
|
||||
|
||||
test("fixLevel:task — fixes roadmap checkbox and UAT stub immediately, defers only summary (#1808)", async () => {
|
||||
const tmp = makeTmp("partial-deferral");
|
||||
try {
|
||||
buildScaffold(tmp);
|
||||
|
||||
const report = await runGSDDoctor(tmp, { fix: true, fixLevel: "task" });
|
||||
|
||||
// Should detect all three issues
|
||||
const codes = report.issues.map(i => i.code);
|
||||
assert.ok(codes.includes("all_tasks_done_missing_slice_summary"), "should detect missing summary");
|
||||
assert.ok(codes.includes("all_tasks_done_missing_slice_uat"), "should detect missing UAT");
|
||||
assert.ok(codes.includes("all_tasks_done_roadmap_not_checked"), "should detect unchecked roadmap");
|
||||
|
||||
// Summary should NOT be created (still deferred — needs LLM content)
|
||||
const sliceSummaryPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md");
|
||||
assert.ok(!existsSync(sliceSummaryPath), "should NOT have created summary stub (deferred)");
|
||||
|
||||
// UAT stub SHOULD be created (mechanical bookkeeping, no longer deferred)
|
||||
const sliceUatPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md");
|
||||
assert.ok(existsSync(sliceUatPath), "should have created UAT stub immediately");
|
||||
|
||||
// Roadmap checkbox SHOULD be marked done (mechanical bookkeeping, no longer deferred)
|
||||
const roadmapContent = readFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf8");
|
||||
assert.ok(roadmapContent.includes("- [x] **S01"), "roadmap should show S01 as checked");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("fixLevel:task — session crash after last task leaves roadmap and UAT consistent (#1808)", async () => {
|
||||
const tmp = makeTmp("crash-consistency");
|
||||
try {
|
||||
buildScaffold(tmp);
|
||||
|
||||
// Simulate: doctor runs at task level (as auto-mode does after last task)
|
||||
await runGSDDoctor(tmp, { fix: true, fixLevel: "task" });
|
||||
|
||||
// Now simulate a session crash — no complete-slice ever runs.
|
||||
// A new session starts and runs doctor again at task level.
|
||||
const report2 = await runGSDDoctor(tmp, { fix: true, fixLevel: "task" });
|
||||
|
||||
// The only remaining issue should be the deferred summary.
|
||||
// Roadmap and UAT should already be fixed from the first run.
|
||||
const remainingCodes = report2.issues.map(i => i.code);
|
||||
assert.ok(
|
||||
!remainingCodes.includes("all_tasks_done_roadmap_not_checked"),
|
||||
"roadmap should already be fixed from first doctor run"
|
||||
);
|
||||
assert.ok(
|
||||
!remainingCodes.includes("all_tasks_done_missing_slice_uat"),
|
||||
"UAT should already be fixed from first doctor run"
|
||||
);
|
||||
// Summary is still missing (deferred), that is expected
|
||||
assert.ok(
|
||||
remainingCodes.includes("all_tasks_done_missing_slice_summary"),
|
||||
"summary should still be detected as missing (deferred)"
|
||||
);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ Done.
|
|||
`);
|
||||
}
|
||||
|
||||
test("fixLevel:task — detects completion issues but does NOT create summary stub or mark roadmap", async () => {
|
||||
test("fixLevel:task — defers only summary stub, fixes roadmap and UAT immediately (#1808)", async () => {
|
||||
const tmp = makeTmp("task-level");
|
||||
try {
|
||||
buildScaffold(tmp);
|
||||
|
|
@ -75,17 +75,17 @@ test("fixLevel:task — detects completion issues but does NOT create summary st
|
|||
assert.ok(codes.includes("all_tasks_done_missing_slice_summary"), "should detect missing summary");
|
||||
assert.ok(codes.includes("all_tasks_done_roadmap_not_checked"), "should detect unchecked roadmap");
|
||||
|
||||
// Should NOT have fixed them
|
||||
// Summary should NOT be created (still deferred — needs LLM content)
|
||||
const sliceSummaryPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md");
|
||||
assert.ok(!existsSync(sliceSummaryPath), "should NOT have created summary stub");
|
||||
|
||||
// Roadmap SHOULD be marked done (mechanical bookkeeping, no longer deferred)
|
||||
const roadmapContent = readFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf8");
|
||||
assert.ok(roadmapContent.includes("- [ ] **S01"), "roadmap should still show S01 as unchecked");
|
||||
assert.ok(roadmapContent.includes("- [x] **S01"), "roadmap should show S01 as checked");
|
||||
|
||||
// Fixes applied should NOT include completion artifacts
|
||||
// Fixes applied should NOT include summary but SHOULD include roadmap
|
||||
for (const f of report.fixesApplied) {
|
||||
assert.ok(!f.includes("SUMMARY"), `should not have fixed summary: ${f}`);
|
||||
assert.ok(!f.includes("roadmap"), `should not have fixed roadmap: ${f}`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue