gsd-pi 2.79.0-dev.ece5fd8ba → 2.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loader.js +0 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +55 -6
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +45 -52
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto.js +159 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-ff639266d978f2a0.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/package.json +2 -2
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
- package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
- package/src/resources/extensions/gsd/auto/phases.ts +82 -8
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -50
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto.ts +167 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- package/dist/web/standalone/.next/static/chunks/app/page-fab3ebb85b006001.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
- /package/dist/web/standalone/.next/static/{TzEVJ1Lh8vbez4n4Q9TqQ → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{TzEVJ1Lh8vbez4n4Q9TqQ → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { createAutoOrchestrator } from "../auto/orchestrator.js";
|
|
5
|
+
import type { AutoOrchestratorDeps } from "../auto/contracts.js";
|
|
6
|
+
|
|
7
|
+
function makeDeps(overrides: Partial<AutoOrchestratorDeps> = {}): { deps: AutoOrchestratorDeps; calls: string[] } {
|
|
8
|
+
const calls: string[] = [];
|
|
9
|
+
|
|
10
|
+
const deps: AutoOrchestratorDeps = {
|
|
11
|
+
dispatch: {
|
|
12
|
+
async decideNextUnit() {
|
|
13
|
+
calls.push("dispatch.decide");
|
|
14
|
+
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
recovery: {
|
|
18
|
+
async classifyAndRecover() {
|
|
19
|
+
calls.push("recovery.classify");
|
|
20
|
+
return { action: "stop", reason: "fatal" };
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
worktree: {
|
|
24
|
+
async prepareForUnit() { calls.push("worktree.prepare"); },
|
|
25
|
+
async syncAfterUnit() { calls.push("worktree.sync"); },
|
|
26
|
+
async cleanupOnStop() { calls.push("worktree.cleanup"); },
|
|
27
|
+
},
|
|
28
|
+
health: {
|
|
29
|
+
async preAdvanceGate() {
|
|
30
|
+
calls.push("health.pre");
|
|
31
|
+
return { allow: true };
|
|
32
|
+
},
|
|
33
|
+
async postAdvanceRecord() { calls.push("health.post"); },
|
|
34
|
+
},
|
|
35
|
+
runtime: {
|
|
36
|
+
async ensureLockOwnership() { calls.push("runtime.lock"); },
|
|
37
|
+
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
38
|
+
},
|
|
39
|
+
notifications: {
|
|
40
|
+
async notifyLifecycle(event) { calls.push(`notify:${event.name}`); },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return { deps: { ...deps, ...overrides }, calls };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("start() advances and records active unit", async () => {
|
|
48
|
+
const { deps, calls } = makeDeps();
|
|
49
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
50
|
+
|
|
51
|
+
const result = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
52
|
+
|
|
53
|
+
assert.equal(result.kind, "advanced");
|
|
54
|
+
const status = orchestrator.getStatus();
|
|
55
|
+
assert.equal(status.phase, "running");
|
|
56
|
+
assert.deepEqual(status.activeUnit, { unitType: "execute-task", unitId: "T01" });
|
|
57
|
+
assert.ok(calls.includes("journal:start"));
|
|
58
|
+
assert.ok(calls.includes("journal:advance"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("advance() returns blocked when health gate denies", async () => {
|
|
62
|
+
const { deps } = makeDeps({
|
|
63
|
+
health: {
|
|
64
|
+
async preAdvanceGate() { return { allow: false, reason: "doctor-block" }; },
|
|
65
|
+
async postAdvanceRecord() {},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
69
|
+
|
|
70
|
+
const result = await orchestrator.advance();
|
|
71
|
+
|
|
72
|
+
assert.equal(result.kind, "blocked");
|
|
73
|
+
assert.equal(result.reason, "doctor-block");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("advance() stops when dispatch has no next unit", async () => {
|
|
77
|
+
const { deps } = makeDeps({
|
|
78
|
+
dispatch: {
|
|
79
|
+
async decideNextUnit() { return null; },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
83
|
+
|
|
84
|
+
const result = await orchestrator.advance();
|
|
85
|
+
|
|
86
|
+
assert.equal(result.kind, "stopped");
|
|
87
|
+
assert.equal(orchestrator.getStatus().phase, "stopped");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("advance() uses recovery on error", async () => {
|
|
91
|
+
const { deps, calls } = makeDeps({
|
|
92
|
+
runtime: {
|
|
93
|
+
async ensureLockOwnership() { throw new Error("lock lost"); },
|
|
94
|
+
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
95
|
+
},
|
|
96
|
+
recovery: {
|
|
97
|
+
async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
101
|
+
|
|
102
|
+
const result = await orchestrator.advance();
|
|
103
|
+
|
|
104
|
+
assert.equal(result.kind, "error");
|
|
105
|
+
assert.equal(result.reason, "needs manual");
|
|
106
|
+
assert.equal(orchestrator.getStatus().phase, "error");
|
|
107
|
+
assert.ok(calls.includes("journal:advance-error"));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("advance() is idempotent for the same active unit", async () => {
|
|
111
|
+
const { deps, calls } = makeDeps();
|
|
112
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
113
|
+
|
|
114
|
+
const first = await orchestrator.advance();
|
|
115
|
+
const second = await orchestrator.advance();
|
|
116
|
+
|
|
117
|
+
assert.equal(first.kind, "advanced");
|
|
118
|
+
assert.equal(second.kind, "blocked");
|
|
119
|
+
assert.equal(second.reason, "idempotent advance: unit already active");
|
|
120
|
+
|
|
121
|
+
const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
|
|
122
|
+
assert.equal(prepareCalls, 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("resume() re-enters running flow via advance", async () => {
|
|
126
|
+
const { deps } = makeDeps();
|
|
127
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
128
|
+
|
|
129
|
+
const result = await orchestrator.resume();
|
|
130
|
+
|
|
131
|
+
assert.equal(result.kind, "advanced");
|
|
132
|
+
assert.equal(orchestrator.getStatus().phase, "running");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("resume() clears idempotent lock and allows re-advance", async () => {
|
|
136
|
+
const { deps } = makeDeps();
|
|
137
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
138
|
+
|
|
139
|
+
const first = await orchestrator.advance();
|
|
140
|
+
const blocked = await orchestrator.advance();
|
|
141
|
+
const resumed = await orchestrator.resume();
|
|
142
|
+
|
|
143
|
+
assert.equal(first.kind, "advanced");
|
|
144
|
+
assert.equal(blocked.kind, "blocked");
|
|
145
|
+
assert.equal(resumed.kind, "advanced");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("transitionCount increases across lifecycle transitions", async () => {
|
|
149
|
+
const { deps } = makeDeps();
|
|
150
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
151
|
+
|
|
152
|
+
const before = orchestrator.getStatus().transitionCount;
|
|
153
|
+
await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
154
|
+
const afterStart = orchestrator.getStatus().transitionCount;
|
|
155
|
+
await orchestrator.stop("done");
|
|
156
|
+
const afterStop = orchestrator.getStatus().transitionCount;
|
|
157
|
+
|
|
158
|
+
assert.ok(afterStart > before);
|
|
159
|
+
assert.ok(afterStop > afterStart);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("stop() clears idempotent unit lock so advance can run again", async () => {
|
|
163
|
+
const { deps } = makeDeps();
|
|
164
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
165
|
+
|
|
166
|
+
const first = await orchestrator.advance();
|
|
167
|
+
const blocked = await orchestrator.advance();
|
|
168
|
+
const stopped = await orchestrator.stop("reset");
|
|
169
|
+
const second = await orchestrator.advance();
|
|
170
|
+
|
|
171
|
+
assert.equal(first.kind, "advanced");
|
|
172
|
+
assert.equal(blocked.kind, "blocked");
|
|
173
|
+
assert.equal(stopped.kind, "stopped");
|
|
174
|
+
assert.equal(second.kind, "advanced");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("advance() stopped clears previous activeUnit", async () => {
|
|
178
|
+
let first = true;
|
|
179
|
+
const { deps } = makeDeps({
|
|
180
|
+
dispatch: {
|
|
181
|
+
async decideNextUnit() {
|
|
182
|
+
if (first) {
|
|
183
|
+
first = false;
|
|
184
|
+
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
191
|
+
|
|
192
|
+
await orchestrator.advance();
|
|
193
|
+
const stopped = await orchestrator.advance();
|
|
194
|
+
|
|
195
|
+
assert.equal(stopped.kind, "stopped");
|
|
196
|
+
assert.equal(orchestrator.getStatus().activeUnit, undefined);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("recovery stop clears activeUnit", async () => {
|
|
200
|
+
const { deps, calls } = makeDeps({
|
|
201
|
+
runtime: {
|
|
202
|
+
async ensureLockOwnership() { throw new Error("boom"); },
|
|
203
|
+
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
204
|
+
},
|
|
205
|
+
recovery: {
|
|
206
|
+
async classifyAndRecover() { return { action: "stop", reason: "fatal" }; },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
210
|
+
|
|
211
|
+
const result = await orchestrator.advance();
|
|
212
|
+
|
|
213
|
+
assert.equal(result.kind, "stopped");
|
|
214
|
+
assert.equal(orchestrator.getStatus().activeUnit, undefined);
|
|
215
|
+
assert.ok(calls.includes("journal:advance-stopped"));
|
|
216
|
+
assert.ok(calls.includes("notify:stopped"));
|
|
217
|
+
assert.ok(!calls.includes("notify:error"));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("recovery retry maps to paused result", async () => {
|
|
221
|
+
const { deps, calls } = makeDeps({
|
|
222
|
+
runtime: {
|
|
223
|
+
async ensureLockOwnership() { throw new Error("boom"); },
|
|
224
|
+
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
225
|
+
},
|
|
226
|
+
recovery: {
|
|
227
|
+
async classifyAndRecover() { return { action: "retry", reason: "transient" }; },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
231
|
+
|
|
232
|
+
const result = await orchestrator.advance();
|
|
233
|
+
|
|
234
|
+
assert.equal(result.kind, "paused");
|
|
235
|
+
assert.equal(result.reason, "transient");
|
|
236
|
+
assert.equal(orchestrator.getStatus().phase, "paused");
|
|
237
|
+
assert.ok(calls.includes("journal:advance-paused"));
|
|
238
|
+
assert.ok(calls.includes("notify:pause"));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("getStatus() returns defensive copy of activeUnit", async () => {
|
|
242
|
+
const { deps } = makeDeps();
|
|
243
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
244
|
+
|
|
245
|
+
await orchestrator.advance();
|
|
246
|
+
const snap1 = orchestrator.getStatus();
|
|
247
|
+
if (snap1.activeUnit) snap1.activeUnit.unitId = "MUTATED";
|
|
248
|
+
const snap2 = orchestrator.getStatus();
|
|
249
|
+
|
|
250
|
+
assert.equal(snap2.activeUnit?.unitId, "T01");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("start() clears prior idempotent lock", async () => {
|
|
254
|
+
const { deps } = makeDeps();
|
|
255
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
256
|
+
|
|
257
|
+
await orchestrator.advance();
|
|
258
|
+
const blocked = await orchestrator.advance();
|
|
259
|
+
const restarted = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
260
|
+
|
|
261
|
+
assert.equal(blocked.kind, "blocked");
|
|
262
|
+
assert.equal(restarted.kind, "advanced");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("error path emits error notification", async () => {
|
|
266
|
+
const { deps, calls } = makeDeps({
|
|
267
|
+
runtime: {
|
|
268
|
+
async ensureLockOwnership() { throw new Error("boom"); },
|
|
269
|
+
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
270
|
+
},
|
|
271
|
+
recovery: {
|
|
272
|
+
async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
276
|
+
|
|
277
|
+
await orchestrator.advance();
|
|
278
|
+
|
|
279
|
+
assert.ok(calls.includes("notify:error"));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("blocked path journals advance-blocked", async () => {
|
|
283
|
+
const { deps, calls } = makeDeps();
|
|
284
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
285
|
+
|
|
286
|
+
await orchestrator.advance();
|
|
287
|
+
await orchestrator.advance();
|
|
288
|
+
|
|
289
|
+
assert.ok(calls.includes("journal:advance-blocked"));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("health post hook runs on blocked result", async () => {
|
|
293
|
+
const { deps, calls } = makeDeps();
|
|
294
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
295
|
+
|
|
296
|
+
await orchestrator.advance();
|
|
297
|
+
await orchestrator.advance();
|
|
298
|
+
|
|
299
|
+
assert.ok(calls.includes("health.post"));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("start() emits start notification", async () => {
|
|
303
|
+
const { deps, calls } = makeDeps();
|
|
304
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
305
|
+
|
|
306
|
+
await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
307
|
+
|
|
308
|
+
assert.ok(calls.includes("notify:start"));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("resume() emits resume notification", async () => {
|
|
312
|
+
const { deps, calls } = makeDeps();
|
|
313
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
314
|
+
|
|
315
|
+
await orchestrator.resume();
|
|
316
|
+
|
|
317
|
+
assert.ok(calls.includes("notify:resume"));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("stopped with no remaining units clears idempotent lock for next advance", async () => {
|
|
321
|
+
let callCount = 0;
|
|
322
|
+
const { deps } = makeDeps({
|
|
323
|
+
dispatch: {
|
|
324
|
+
async decideNextUnit() {
|
|
325
|
+
callCount += 1;
|
|
326
|
+
if (callCount === 2) return null;
|
|
327
|
+
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
332
|
+
|
|
333
|
+
const first = await orchestrator.advance();
|
|
334
|
+
const stopped = await orchestrator.advance();
|
|
335
|
+
const after = await orchestrator.advance();
|
|
336
|
+
|
|
337
|
+
assert.equal(first.kind, "advanced");
|
|
338
|
+
assert.equal(stopped.kind, "stopped");
|
|
339
|
+
assert.equal(after.kind, "advanced");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("stop() cleans up worktree and transitions to stopped", async () => {
|
|
343
|
+
const { deps, calls } = makeDeps();
|
|
344
|
+
const orchestrator = createAutoOrchestrator(deps);
|
|
345
|
+
|
|
346
|
+
const result = await orchestrator.stop("user-request");
|
|
347
|
+
|
|
348
|
+
assert.equal(result.kind, "stopped");
|
|
349
|
+
assert.equal(orchestrator.getStatus().phase, "stopped");
|
|
350
|
+
assert.ok(calls.includes("worktree.cleanup"));
|
|
351
|
+
assert.ok(calls.includes("journal:stop"));
|
|
352
|
+
assert.ok(calls.includes("notify:stop"));
|
|
353
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { autoSession, getAutoRuntimeSnapshot } from "../auto-runtime-state.ts";
|
|
5
|
+
|
|
6
|
+
test("getAutoRuntimeSnapshot includes orchestration phase when available", () => {
|
|
7
|
+
autoSession.reset();
|
|
8
|
+
autoSession.active = true;
|
|
9
|
+
autoSession.basePath = "/tmp/project";
|
|
10
|
+
autoSession.orchestration = {
|
|
11
|
+
async start() { return { kind: "advanced" as const }; },
|
|
12
|
+
async advance() { return { kind: "advanced" as const }; },
|
|
13
|
+
async resume() { return { kind: "advanced" as const }; },
|
|
14
|
+
async stop() { return { kind: "stopped" as const }; },
|
|
15
|
+
getStatus() {
|
|
16
|
+
return { phase: "running" as const, transitionCount: 3, lastTransitionAt: 123 };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const snap = getAutoRuntimeSnapshot();
|
|
21
|
+
|
|
22
|
+
assert.equal(snap.active, true);
|
|
23
|
+
assert.equal(snap.basePath, "/tmp/project");
|
|
24
|
+
assert.equal(snap.orchestrationPhase, "running");
|
|
25
|
+
assert.equal(snap.orchestrationTransitionCount, 3);
|
|
26
|
+
assert.equal(snap.orchestrationLastTransitionAt, 123);
|
|
27
|
+
|
|
28
|
+
autoSession.reset();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("getAutoRuntimeSnapshot omits orchestration phase when seam not wired", () => {
|
|
32
|
+
autoSession.reset();
|
|
33
|
+
|
|
34
|
+
const snap = getAutoRuntimeSnapshot();
|
|
35
|
+
|
|
36
|
+
assert.equal(snap.orchestrationPhase, undefined);
|
|
37
|
+
assert.equal(snap.orchestrationTransitionCount, undefined);
|
|
38
|
+
assert.equal(snap.orchestrationLastTransitionAt, undefined);
|
|
39
|
+
});
|
|
@@ -201,6 +201,9 @@ test("AutoSession.toJSON() includes key diagnostic properties", () => {
|
|
|
201
201
|
"basePath",
|
|
202
202
|
"currentMilestoneId",
|
|
203
203
|
"currentUnit",
|
|
204
|
+
"orchestrationPhase",
|
|
205
|
+
"orchestrationTransitionCount",
|
|
206
|
+
"orchestrationLastTransitionAt",
|
|
204
207
|
];
|
|
205
208
|
|
|
206
209
|
const missing = requiredDiagnostics.filter(prop => !toJSONBody.includes(prop));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD Extension - Database regression tests.
|
|
2
|
+
|
|
1
3
|
import { describe, test } from 'node:test';
|
|
2
4
|
import assert from 'node:assert/strict';
|
|
3
5
|
import * as fs from 'node:fs';
|
|
@@ -29,7 +31,10 @@ import {
|
|
|
29
31
|
getTask,
|
|
30
32
|
getSliceTasks,
|
|
31
33
|
checkpointDatabase,
|
|
34
|
+
refreshOpenDatabaseFromDisk,
|
|
35
|
+
tryCreateMemoriesFts,
|
|
32
36
|
} from '../gsd-db.ts';
|
|
37
|
+
import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
|
|
33
38
|
|
|
34
39
|
const _require = createRequire(import.meta.url);
|
|
35
40
|
|
|
@@ -910,6 +915,31 @@ describe('gsd-db', () => {
|
|
|
910
915
|
closeDatabase();
|
|
911
916
|
});
|
|
912
917
|
|
|
918
|
+
test('gsd-db: FTS5 unavailable warning normalizes provider typo', () => {
|
|
919
|
+
const previousStderr = setStderrLoggingEnabled(false);
|
|
920
|
+
_resetLogs();
|
|
921
|
+
try {
|
|
922
|
+
const ok = tryCreateMemoriesFts({
|
|
923
|
+
exec(): void {
|
|
924
|
+
throw new Error('no such moduel : fts5');
|
|
925
|
+
},
|
|
926
|
+
prepare(): never {
|
|
927
|
+
throw new Error('prepare should not be called');
|
|
928
|
+
},
|
|
929
|
+
close(): void {},
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
assert.equal(ok, false, 'FTS5 creation should report fallback');
|
|
933
|
+
const warning = peekLogs().find((entry) => entry.component === 'db' && entry.message.includes('FTS5 unavailable'));
|
|
934
|
+
assert.ok(warning, 'FTS5 fallback warning should be logged');
|
|
935
|
+
assert.match(warning!.message, /no such module: fts5/);
|
|
936
|
+
assert.doesNotMatch(warning!.message, /moduel/);
|
|
937
|
+
} finally {
|
|
938
|
+
_resetLogs();
|
|
939
|
+
setStderrLoggingEnabled(previousStderr);
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
913
943
|
// ─── checkpointDatabase ────────────────────────────────────────────────────
|
|
914
944
|
|
|
915
945
|
describe('checkpointDatabase', () => {
|
|
@@ -952,6 +982,71 @@ describe('gsd-db', () => {
|
|
|
952
982
|
});
|
|
953
983
|
});
|
|
954
984
|
|
|
985
|
+
// ─── refreshOpenDatabaseFromDisk ───────────────────────────────────────────
|
|
986
|
+
|
|
987
|
+
describe('refreshOpenDatabaseFromDisk', () => {
|
|
988
|
+
test('refreshOpenDatabaseFromDisk: reopens the active file-backed database and sees external writes', (t) => {
|
|
989
|
+
const dbPath = tempDbPath();
|
|
990
|
+
t.after(() => cleanup(dbPath));
|
|
991
|
+
|
|
992
|
+
openDatabase(dbPath);
|
|
993
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
994
|
+
insertSlice({
|
|
995
|
+
id: 'S01',
|
|
996
|
+
milestoneId: 'M001',
|
|
997
|
+
title: 'Slice 1',
|
|
998
|
+
status: 'pending',
|
|
999
|
+
sequence: 1,
|
|
1000
|
+
});
|
|
1001
|
+
insertTask({
|
|
1002
|
+
id: 'T01',
|
|
1003
|
+
milestoneId: 'M001',
|
|
1004
|
+
sliceId: 'S01',
|
|
1005
|
+
title: 'Task 1',
|
|
1006
|
+
status: 'pending',
|
|
1007
|
+
sequence: 1,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const adapterBefore = _getAdapter()!;
|
|
1011
|
+
|
|
1012
|
+
const externalDb = openRawSqliteForTest(dbPath);
|
|
1013
|
+
try {
|
|
1014
|
+
externalDb.exec(`
|
|
1015
|
+
INSERT INTO tasks (milestone_id, slice_id, id, title, status, sequence)
|
|
1016
|
+
VALUES ('M001', 'S01', 'T02', 'Task 2', 'pending', 2)
|
|
1017
|
+
`);
|
|
1018
|
+
} finally {
|
|
1019
|
+
externalDb.close();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const visibleBeforeRefresh = getSliceTasks('M001', 'S01').map(task => task.id);
|
|
1023
|
+
assert.ok(visibleBeforeRefresh.includes('T01'));
|
|
1024
|
+
|
|
1025
|
+
assert.equal(refreshOpenDatabaseFromDisk(), true);
|
|
1026
|
+
assert.notEqual(_getAdapter(), adapterBefore, 'refresh must replace the active adapter rather than becoming a no-op');
|
|
1027
|
+
const sliceTaskIds = getSliceTasks('M001', 'S01').map(task => task.id);
|
|
1028
|
+
assert.deepEqual(sliceTaskIds, ['T01', 'T02']);
|
|
1029
|
+
assert.equal(isDbAvailable(), true);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test('refreshOpenDatabaseFromDisk: refuses in-memory databases without closing them', () => {
|
|
1033
|
+
openDatabase(':memory:');
|
|
1034
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
1035
|
+
|
|
1036
|
+
assert.equal(refreshOpenDatabaseFromDisk(), false);
|
|
1037
|
+
assert.equal(isDbAvailable(), true);
|
|
1038
|
+
assert.ok(_getAdapter()!.prepare("SELECT 1 FROM milestones WHERE id = 'M001'").get());
|
|
1039
|
+
|
|
1040
|
+
closeDatabase();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test('refreshOpenDatabaseFromDisk: is a no-op when no database is open', () => {
|
|
1044
|
+
closeDatabase();
|
|
1045
|
+
assert.equal(refreshOpenDatabaseFromDisk(), false);
|
|
1046
|
+
assert.equal(isDbAvailable(), false);
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
|
|
955
1050
|
// ─── getDbStatus ───────────────────────────────────────────────────────────
|
|
956
1051
|
|
|
957
1052
|
describe('getDbStatus', () => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD Extension — Auto recovery integration tests.
|
|
2
|
+
|
|
1
3
|
import test from "node:test";
|
|
2
4
|
import assert from "node:assert/strict";
|
|
3
5
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, chmodSync } from "node:fs";
|
|
@@ -401,6 +403,57 @@ test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", (t
|
|
|
401
403
|
assert.equal(result, false, "should fail when plan has no task entries (empty scaffold, #699)");
|
|
402
404
|
});
|
|
403
405
|
|
|
406
|
+
test("verifyExpectedArtifact plan-slice trusts DB tasks over legacy plan syntax", (t) => {
|
|
407
|
+
const base = makeTmpBase();
|
|
408
|
+
t.after(() => {
|
|
409
|
+
closeDatabase();
|
|
410
|
+
cleanup(base);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
414
|
+
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
|
|
415
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
416
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
417
|
+
insertTask({ id: "T02", milestoneId: "M001", sliceId: "S01", title: "Second task", status: "pending" });
|
|
418
|
+
|
|
419
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
420
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
421
|
+
writeFileSync(
|
|
422
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
423
|
+
"# S01: Slice\n\n## Tasks\n\nTask rows live in the DB; this projection intentionally has no legacy task syntax.\n",
|
|
424
|
+
);
|
|
425
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
426
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan\n");
|
|
427
|
+
|
|
428
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
429
|
+
assert.equal(result, true, "DB task rows plus task plan files should verify plan-slice");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("verifyExpectedArtifact plan-slice still fails when a DB-backed task plan file is missing", (t) => {
|
|
433
|
+
const base = makeTmpBase();
|
|
434
|
+
t.after(() => {
|
|
435
|
+
closeDatabase();
|
|
436
|
+
cleanup(base);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
440
|
+
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
|
|
441
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
442
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
443
|
+
insertTask({ id: "T02", milestoneId: "M001", sliceId: "S01", title: "Second task", status: "pending" });
|
|
444
|
+
|
|
445
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
446
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
447
|
+
writeFileSync(
|
|
448
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
449
|
+
"# S01: Slice\n\n## Tasks\n\nTask rows live in the DB; this projection intentionally has no legacy task syntax.\n",
|
|
450
|
+
);
|
|
451
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
452
|
+
|
|
453
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
454
|
+
assert.equal(result, false, "DB task rows must still require matching task plan files");
|
|
455
|
+
});
|
|
456
|
+
|
|
404
457
|
// ─── verifyExpectedArtifact: heading-style plan tasks (#1691) ─────────────
|
|
405
458
|
|
|
406
459
|
test("verifyExpectedArtifact accepts plan-slice with heading-style tasks (### T01 --)", (t) => {
|
|
@@ -456,6 +509,32 @@ test("verifyExpectedArtifact accepts plan-slice with colon-style heading tasks (
|
|
|
456
509
|
);
|
|
457
510
|
});
|
|
458
511
|
|
|
512
|
+
test("verifyExpectedArtifact accepts indented legacy plan-slice task markers", (t) => {
|
|
513
|
+
const base = makeTmpBase();
|
|
514
|
+
t.after(() => cleanup(base));
|
|
515
|
+
|
|
516
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
517
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
518
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
519
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), [
|
|
520
|
+
"# S01: Test Slice",
|
|
521
|
+
"",
|
|
522
|
+
"## Tasks",
|
|
523
|
+
"",
|
|
524
|
+
" - [ ] **T01: Implement feature** `est:1h`",
|
|
525
|
+
"",
|
|
526
|
+
" ### T02 -- Write tests",
|
|
527
|
+
].join("\n"));
|
|
528
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
|
|
529
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
|
|
530
|
+
|
|
531
|
+
assert.strictEqual(
|
|
532
|
+
verifyExpectedArtifact("plan-slice", "M001/S01", base),
|
|
533
|
+
true,
|
|
534
|
+
"Indented legacy task markers should be treated as completed plan-slice artifacts",
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
459
538
|
test("verifyExpectedArtifact execute-task rejects heading-style plan without checked checkbox (#3607)", (t) => {
|
|
460
539
|
const base = makeTmpBase();
|
|
461
540
|
t.after(() => cleanup(base));
|