feat(S04/T02): Migrate dispatch-guard.ts to DB queries with isDbAvailab…
- src/resources/extensions/gsd/dispatch-guard.ts - src/resources/extensions/gsd/tests/dispatch-guard.test.ts - .gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md
This commit is contained in:
parent
f86882bde5
commit
08c3fcc57c
6 changed files with 239 additions and 65 deletions
|
|
@ -50,7 +50,7 @@
|
|||
- Verify: `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts`
|
||||
- Done when: All 6 ORDER BY queries use `sequence, id`, test file passes, existing tests unbroken
|
||||
|
||||
- [ ] **T02: Migrate dispatch-guard.ts to DB queries and update tests** `est:45m`
|
||||
- [x] **T02: Migrate dispatch-guard.ts to DB queries and update tests** `est:45m`
|
||||
- Why: dispatch-guard re-parses ROADMAP.md on every slice dispatch — the single hottest parser caller. R009 requires this migration.
|
||||
- Files: `src/resources/extensions/gsd/dispatch-guard.ts`, `src/resources/extensions/gsd/tests/dispatch-guard.test.ts`
|
||||
- Do: Replace `parseRoadmapSlices(roadmapContent)` with `getMilestoneSlices(mid)`. Map `SliceRow.status === 'complete'` to `done: true`. Remove `readRoadmapFromDisk()`, `readFileSync`, and `parseRoadmapSlices` imports. Add `isDbAvailable()` + `getMilestoneSlices()` import from `gsd-db.js`. Keep the `findMilestoneIds()` disk-based milestone discovery (DB doesn't own milestone queue order). Add fallback to disk parsing when `!isDbAvailable()`. Update all 8 test cases to seed DB via `openDatabase`/`insertMilestone`/`insertSlice` instead of writing ROADMAP markdown files. Preserve all existing assertion semantics.
|
||||
|
|
|
|||
18
.gsd/milestones/M001/slices/S04/tasks/T01-VERIFY.json
Normal file
18
.gsd/milestones/M001/slices/S04/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M001/S04/T01",
|
||||
"timestamp": 1774285048330,
|
||||
"passed": false,
|
||||
"discoverySource": "package-json",
|
||||
"checks": [
|
||||
{
|
||||
"command": "npm run test",
|
||||
"exitCode": 1,
|
||||
"durationMs": 39381,
|
||||
"verdict": "fail"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
|
|
@ -51,3 +51,10 @@ Replace `parseRoadmapSlices()` in `dispatch-guard.ts` with `getMilestoneSlices()
|
|||
|
||||
- `src/resources/extensions/gsd/dispatch-guard.ts` — migrated to DB queries with disk fallback
|
||||
- `src/resources/extensions/gsd/tests/dispatch-guard.test.ts` — updated to seed DB state
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- **Signal change**: `getPriorSliceCompletionBlocker()` now reads slice status from `slices` table via `getMilestoneSlices()` when DB is open, instead of parsing ROADMAP.md from disk. The returned blocker string is unchanged — callers see no difference.
|
||||
- **Inspection**: To verify DB path is active, check that `isDbAvailable()` returns `true` before calling `getPriorSliceCompletionBlocker()`. Inspect the `slices` table (`SELECT id, status, depends FROM slices WHERE milestone_id = ?`) to see exactly what the guard evaluates.
|
||||
- **Fallback visibility**: When DB is unavailable, the guard falls back to disk parsing via `lazyParseRoadmapSlices()`. No stderr warning is emitted from this function (the `isDbAvailable()` check is silent), but downstream callers can detect fallback by checking `isDbAvailable()` before dispatch.
|
||||
- **Failure state**: If `getMilestoneSlices()` returns an empty array for a milestone that has slices on disk, the guard silently skips that milestone (same as when no ROADMAP file exists). This is safe — it means no blocking, not false blocking.
|
||||
|
|
|
|||
72
.gsd/milestones/M001/slices/S04/tasks/T02-SUMMARY.md
Normal file
72
.gsd/milestones/M001/slices/S04/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S04
|
||||
milestone: M001
|
||||
key_files:
|
||||
- src/resources/extensions/gsd/dispatch-guard.ts
|
||||
- src/resources/extensions/gsd/tests/dispatch-guard.test.ts
|
||||
- .gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md
|
||||
key_decisions:
|
||||
- Used createRequire with try .ts/.js fallback for lazy parser loading instead of dynamic import() — keeps getPriorSliceCompletionBlocker synchronous, avoiding cascading async changes to loop-deps.ts, phases.ts, and all test mocks
|
||||
- Kept minimal ROADMAP stub files on disk in tests because findMilestoneIds() reads milestone directories from disk for queue ordering — DB migration of milestone discovery is out of scope for this task
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-23T17:03:27.608Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Migrate dispatch-guard.ts to DB queries with isDbAvailable() gate and lazy disk-parse fallback
|
||||
|
||||
**Migrate dispatch-guard.ts to DB queries with isDbAvailable() gate and lazy disk-parse fallback**
|
||||
|
||||
## What Happened
|
||||
|
||||
Migrated `getPriorSliceCompletionBlocker()` in `dispatch-guard.ts` from parsing ROADMAP.md files via `parseRoadmapSlices()` to querying the `slices` table via `getMilestoneSlices()` from `gsd-db.ts`.
|
||||
|
||||
**dispatch-guard.ts changes:**
|
||||
- Replaced module-level `parseRoadmapSlices` import with `isDbAvailable()` + `getMilestoneSlices()` from `gsd-db.js`
|
||||
- Added `isDbAvailable()` gate: when DB is open, maps `SliceRow[]` to normalised `{id, done, depends}` objects; when DB is unavailable, falls back to disk parsing via a lazy `createRequire`-based loader
|
||||
- The lazy loader (`lazyParseRoadmapSlices`) uses `createRequire(import.meta.url)` and tries `.ts` first (strip-types dev), then `.js` (compiled production) — avoids module-level import of the parser
|
||||
- Removed unused `readdirSync` and `milestonesDir` imports; kept `readFileSync` for the disk fallback path
|
||||
- Function signature and return type unchanged — no cascading changes to callers
|
||||
|
||||
**dispatch-guard.test.ts changes:**
|
||||
- All 8 test cases now seed state via `openDatabase()` + `insertMilestone()` + `insertSlice()` instead of writing ROADMAP markdown files
|
||||
- Added `setupRepo()` / `teardownRepo()` helpers for consistent DB lifecycle (open before test, close in finally)
|
||||
- Milestone directory + minimal ROADMAP stub still written for `findMilestoneIds()` which reads disk for milestone discovery
|
||||
- SUMMARY file still written on disk for the SUMMARY-skip test (dispatch-guard checks `resolveMilestoneFile`)
|
||||
|
||||
**Integration tests:** The `integration-mixed-milestones.test.ts` suite (54 sub-tests) passes — these tests don't seed DB, so they exercise the disk-parse fallback path, confirming both code paths work.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `dispatch-guard.test.ts` — all 8 tests pass with DB seeding
|
||||
2. `integration-mixed-milestones.test.ts` — all 54 sub-tests pass (exercises fallback path)
|
||||
3. `schema-v9-sequence.test.ts` — all 7 tests pass (T01 regression)
|
||||
4. `grep parseRoadmapSlices dispatch-guard.ts` — only matches in lazy fallback block (lines 17,19), zero module-level imports
|
||||
5. Diagnostic: `getMilestoneSlices('NONEXISTENT')` returns `[]` (no crash on missing milestone)
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/dispatch-guard.test.ts` | 0 | ✅ pass | 614ms |
|
||||
| 2 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts` | 0 | ✅ pass | 749ms |
|
||||
| 3 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts` | 0 | ✅ pass | 137ms |
|
||||
| 4 | `grep -c parseRoadmapSlices dispatch-guard.ts (module-level imports)` | 0 | ✅ pass — only in lazy fallback block | 5ms |
|
||||
| 5 | `node --import resolve-ts.mjs -e 'getMilestoneSlices(NONEXISTENT)' diagnostic` | 0 | ✅ pass — returns [] | 200ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
The task plan suggested removing `readFileSync` import if no longer needed outside fallback — it's still needed for the `readRoadmapFromDisk()` fallback function, so it was kept. The `readdirSync` import and `milestonesDir` import were removed as they were unused. The lazy import approach uses `createRequire` with try/catch for .ts/.js extension resolution instead of a dynamic `import()`, keeping the function synchronous and avoiding cascading async changes to the call chain.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/resources/extensions/gsd/dispatch-guard.ts`
|
||||
- `src/resources/extensions/gsd/tests/dispatch-guard.test.ts`
|
||||
- `.gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md`
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
// GSD Dispatch Guard — prevents out-of-order slice dispatch
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { resolveMilestoneFile, milestonesDir } from "./paths.js";
|
||||
import { parseRoadmapSlices } from "./roadmap-slices.js";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolveMilestoneFile } from "./paths.js";
|
||||
import { findMilestoneIds } from "./guided-flow.js";
|
||||
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
|
||||
|
||||
// Lazy-loaded parser — only resolved when DB is unavailable (fallback path).
|
||||
// Uses createRequire so the function stays synchronous. Tries .ts first (strip-types dev)
|
||||
// then .js (compiled production).
|
||||
let _lazyParser: ((content: string) => { id: string; done: boolean; depends: string[] }[]) | null = null;
|
||||
function lazyParseRoadmapSlices(content: string) {
|
||||
if (!_lazyParser) {
|
||||
const req = createRequire(import.meta.url);
|
||||
try {
|
||||
_lazyParser = req("./roadmap-slices.ts").parseRoadmapSlices;
|
||||
} catch {
|
||||
_lazyParser = req("./roadmap-slices.js").parseRoadmapSlices;
|
||||
}
|
||||
}
|
||||
return _lazyParser!(content);
|
||||
}
|
||||
|
||||
const SLICE_DISPATCH_TYPES = new Set([
|
||||
"research-slice",
|
||||
|
|
@ -58,11 +74,25 @@ export function getPriorSliceCompletionBlocker(
|
|||
if (resolveMilestoneFile(base, mid, "PARKED")) continue;
|
||||
if (resolveMilestoneFile(base, mid, "SUMMARY")) continue;
|
||||
|
||||
// Read from disk (working tree) — always has the latest state
|
||||
const roadmapContent = readRoadmapFromDisk(base, mid);
|
||||
if (!roadmapContent) continue;
|
||||
// Normalised slice list: prefer DB, fall back to disk parsing
|
||||
type NormSlice = { id: string; done: boolean; depends: string[] };
|
||||
let slices: NormSlice[];
|
||||
|
||||
if (isDbAvailable()) {
|
||||
const rows = getMilestoneSlices(mid);
|
||||
if (rows.length === 0) continue;
|
||||
slices = rows.map((r) => ({
|
||||
id: r.id,
|
||||
done: r.status === "complete",
|
||||
depends: r.depends ?? [],
|
||||
}));
|
||||
} else {
|
||||
// Fallback: disk parsing when DB is not yet initialised
|
||||
const roadmapContent = readRoadmapFromDisk(base, mid);
|
||||
if (!roadmapContent) continue;
|
||||
slices = lazyParseRoadmapSlices(roadmapContent);
|
||||
}
|
||||
|
||||
const slices = parseRoadmapSlices(roadmapContent);
|
||||
if (mid !== targetMid) {
|
||||
const incomplete = slices.find((slice) => !slice.done);
|
||||
if (incomplete) {
|
||||
|
|
|
|||
|
|
@ -4,58 +4,92 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { getPriorSliceCompletionBlocker } from "../dispatch-guard.ts";
|
||||
import { openDatabase, closeDatabase, insertMilestone, insertSlice } from "../gsd-db.ts";
|
||||
|
||||
/** Helper: create temp dir and open an in-dir DB for dispatch-guard tests */
|
||||
function setupRepo(): string {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
||||
openDatabase(join(repo, ".gsd", "gsd.db"));
|
||||
return repo;
|
||||
}
|
||||
|
||||
/** Helper: tear down repo (close DB then remove dir) */
|
||||
function teardownRepo(repo: string): void {
|
||||
closeDatabase();
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
test("dispatch guard blocks when prior milestone has incomplete slices", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
|
||||
"# M002: Previous\n\n## Slices\n- [x] **S01: Done** `risk:low` `depends:[]`\n- [ ] **S02: Pending** `risk:low` `depends:[S01]`\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"),
|
||||
"# M003: Current\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n- [ ] **S02: Second** `risk:low` `depends:[S01]`\n");
|
||||
// Seed DB: M002 with S01 complete, S02 pending
|
||||
insertMilestone({ id: "M002", title: "Previous" });
|
||||
insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M002", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
|
||||
|
||||
// M003 with two pending slices
|
||||
insertMilestone({ id: "M003", title: "Current" });
|
||||
insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
|
||||
|
||||
// Need ROADMAP files for milestone discovery (findMilestoneIds reads disk)
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
|
||||
|
||||
assert.equal(
|
||||
getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
|
||||
"Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete.",
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard blocks later slice in same milestone when earlier incomplete", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
|
||||
"# M002: Previous\n\n## Slices\n- [x] **S01: Done** `risk:low` `depends:[]`\n- [x] **S02: Done** `risk:low` `depends:[S01]`\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"),
|
||||
"# M003: Current\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n- [ ] **S02: Second** `risk:low` `depends:[S01]`\n");
|
||||
insertMilestone({ id: "M002", title: "Previous" });
|
||||
insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M002", title: "Done", status: "complete", depends: ["S01"], sequence: 2 });
|
||||
|
||||
insertMilestone({ id: "M003", title: "Current" });
|
||||
insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
|
||||
|
||||
assert.equal(
|
||||
getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
|
||||
"Cannot dispatch execute-task M003/S02/T01: dependency slice M003/S01 is not complete.",
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard allows dispatch when all earlier slices complete", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"),
|
||||
"# M003: Current\n\n## Slices\n- [x] **S01: First** `risk:low` `depends:[]`\n- [ ] **S02: Second** `risk:low` `depends:[S01]`\n");
|
||||
|
||||
insertMilestone({ id: "M003", title: "Current" });
|
||||
insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
|
||||
|
||||
assert.equal(getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"), null);
|
||||
assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"), null);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -63,17 +97,19 @@ test("dispatch guard unblocks slice when positionally-earlier slice depends on i
|
|||
// S05 depends on S06, but S05 appears first positionally.
|
||||
// Old behavior: S06 blocked because S05 (positionally earlier) is incomplete.
|
||||
// Fixed behavior: S06 has no unmet dependencies, so it can dispatch.
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
||||
"# M001: Test\n\n## Slices\n" +
|
||||
"- [x] **S01: Setup** `risk:low` `depends:[]`\n" +
|
||||
"- [x] **S02: Core** `risk:low` `depends:[S01]`\n" +
|
||||
"- [x] **S03: API** `risk:low` `depends:[S02]`\n" +
|
||||
"- [x] **S04: Auth** `risk:low` `depends:[S03]`\n" +
|
||||
"- [ ] **S05: Integration** `risk:high` `depends:[S04,S06]`\n" +
|
||||
"- [ ] **S06: Data Layer** `risk:medium` `depends:[S04]`\n");
|
||||
|
||||
insertMilestone({ id: "M001", title: "Test" });
|
||||
insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
|
||||
insertSlice({ id: "S03", milestoneId: "M001", title: "API", status: "complete", depends: ["S02"], sequence: 3 });
|
||||
insertSlice({ id: "S04", milestoneId: "M001", title: "Auth", status: "complete", depends: ["S03"], sequence: 4 });
|
||||
insertSlice({ id: "S05", milestoneId: "M001", title: "Integration", status: "pending", depends: ["S04", "S06"], sequence: 5 });
|
||||
insertSlice({ id: "S06", milestoneId: "M001", title: "Data Layer", status: "pending", depends: ["S04"], sequence: 6 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
|
||||
|
||||
// S06 depends only on S04 (complete) — should be unblocked
|
||||
assert.equal(
|
||||
|
|
@ -87,19 +123,21 @@ test("dispatch guard unblocks slice when positionally-earlier slice depends on i
|
|||
"Cannot dispatch plan-slice M001/S05: dependency slice M001/S06 is not complete.",
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard falls back to positional ordering when no dependencies declared", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
||||
"# M001: Test\n\n## Slices\n" +
|
||||
"- [x] **S01: First** `risk:low` `depends:[]`\n" +
|
||||
"- [ ] **S02: Second** `risk:low` `depends:[]`\n" +
|
||||
"- [ ] **S03: Third** `risk:low` `depends:[]`\n");
|
||||
|
||||
insertMilestone({ id: "M001", title: "Test" });
|
||||
insertSlice({ id: "S01", milestoneId: "M001", title: "First", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M001", title: "Second", status: "pending", depends: [], sequence: 2 });
|
||||
insertSlice({ id: "S03", milestoneId: "M001", title: "Third", status: "pending", depends: [], sequence: 3 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
|
||||
|
||||
// S03 has no dependencies — positional fallback blocks on S02
|
||||
assert.equal(
|
||||
|
|
@ -113,20 +151,22 @@ test("dispatch guard falls back to positional ordering when no dependencies decl
|
|||
null,
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard allows slice with all declared dependencies complete", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
||||
"# M001: Test\n\n## Slices\n" +
|
||||
"- [x] **S01: Setup** `risk:low` `depends:[]`\n" +
|
||||
"- [x] **S02: Core** `risk:low` `depends:[S01]`\n" +
|
||||
"- [ ] **S03: Feature A** `risk:low` `depends:[S01,S02]`\n" +
|
||||
"- [ ] **S04: Feature B** `risk:low` `depends:[S01]`\n");
|
||||
|
||||
insertMilestone({ id: "M001", title: "Test" });
|
||||
insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
|
||||
insertSlice({ id: "S03", milestoneId: "M001", title: "Feature A", status: "pending", depends: ["S01", "S02"], sequence: 3 });
|
||||
insertSlice({ id: "S04", milestoneId: "M001", title: "Feature B", status: "pending", depends: ["S01"], sequence: 4 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
|
||||
|
||||
// S03 depends on S01 (done) and S02 (done) — unblocked
|
||||
assert.equal(
|
||||
|
|
@ -140,28 +180,31 @@ test("dispatch guard allows slice with all declared dependencies complete", () =
|
|||
null,
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard skips completed milestone with SUMMARY even if it has unchecked remediation slices (#1716)", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
|
||||
|
||||
// M001 is complete (has SUMMARY) but has unchecked remediation slices
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
||||
"# M001: Previous\n\n## Slices\n" +
|
||||
"- [x] **S01: Core** `risk:low` `depends:[]`\n" +
|
||||
"- [x] **S02: Tests** `risk:low` `depends:[S01]`\n" +
|
||||
"- [ ] **S03-R: Remediation** `risk:low` `depends:[S02]`\n" +
|
||||
"- [ ] **S04-R: Remediation 2** `risk:low` `depends:[S02]`\n");
|
||||
// M001 is complete (has SUMMARY) but has unchecked remediation slices in DB
|
||||
insertMilestone({ id: "M001", title: "Previous" });
|
||||
insertSlice({ id: "S01", milestoneId: "M001", title: "Core", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M001", title: "Tests", status: "complete", depends: ["S01"], sequence: 2 });
|
||||
insertSlice({ id: "S03-R", milestoneId: "M001", title: "Remediation", status: "pending", depends: ["S02"], sequence: 3 });
|
||||
insertSlice({ id: "S04-R", milestoneId: "M001", title: "Remediation 2", status: "pending", depends: ["S02"], sequence: 4 });
|
||||
|
||||
insertMilestone({ id: "M002", title: "Current" });
|
||||
insertSlice({ id: "S01", milestoneId: "M002", title: "Start", status: "pending", depends: [], sequence: 1 });
|
||||
|
||||
// M001 SUMMARY on disk triggers skip
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
|
||||
"---\nstatus: complete\n---\n# M001 Summary\nDone.\n");
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
|
||||
"# M002: Current\n\n## Slices\n- [ ] **S01: Start** `risk:low` `depends:[]`\n");
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
|
||||
|
||||
// M001 has SUMMARY — should be skipped, not block M002/S01
|
||||
assert.equal(
|
||||
|
|
@ -169,19 +212,23 @@ test("dispatch guard skips completed milestone with SUMMARY even if it has unche
|
|||
null,
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatch guard works without git repo", () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-nogit-"));
|
||||
const repo = setupRepo();
|
||||
try {
|
||||
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
||||
"# M001: Test\n\n## Slices\n- [x] **S01: Done** `risk:low` `depends:[]`\n- [ ] **S02: Pending** `risk:low` `depends:[S01]`\n");
|
||||
|
||||
insertMilestone({ id: "M001", title: "Test" });
|
||||
insertSlice({ id: "S01", milestoneId: "M001", title: "Done", status: "complete", depends: [], sequence: 1 });
|
||||
insertSlice({ id: "S02", milestoneId: "M001", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
|
||||
|
||||
writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
|
||||
|
||||
assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S02"), null);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
teardownRepo(repo);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue