gsd-pi 2.79.0 → 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/README.md +94 -47
- 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 +61 -7
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- 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/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +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/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- 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 +88 -9
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- 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/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- 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-recovery.test.ts +108 -1
- 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/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -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/parallel-skill-prompt-integration.test.ts +8 -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/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
showSmartEntry,
|
|
24
24
|
startDeepProjectSetupForeground,
|
|
25
25
|
} from "../guided-flow.ts";
|
|
26
|
+
import {
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertMilestone,
|
|
29
|
+
openDatabase,
|
|
30
|
+
} from "../gsd-db.ts";
|
|
26
31
|
import type { GSDPreferences } from "../preferences.ts";
|
|
27
32
|
import type { GSDState } from "../types.ts";
|
|
28
33
|
|
|
@@ -342,6 +347,62 @@ test("deep project setup: pre-dispatch can run before the first milestone exists
|
|
|
342
347
|
}
|
|
343
348
|
});
|
|
344
349
|
|
|
350
|
+
test("deep project setup: bootstrap continues queued M002 without milestone context", async () => {
|
|
351
|
+
const base = makeRepo();
|
|
352
|
+
try {
|
|
353
|
+
writeCapturedDeepPrefs(base);
|
|
354
|
+
writeValidProjectAndRequirements(base);
|
|
355
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
356
|
+
writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), '{"decision":"skip"}\n');
|
|
357
|
+
|
|
358
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
359
|
+
insertMilestone({ id: "M001", title: "First milestone", status: "complete" });
|
|
360
|
+
insertMilestone({ id: "M002", title: "Second milestone", status: "queued" });
|
|
361
|
+
closeDatabase();
|
|
362
|
+
|
|
363
|
+
const messages: unknown[] = [];
|
|
364
|
+
const pi = {
|
|
365
|
+
...makePi(messages),
|
|
366
|
+
getThinkingLevel: () => "medium",
|
|
367
|
+
};
|
|
368
|
+
const s = new AutoSession();
|
|
369
|
+
const ready = await bootstrapAutoSession(
|
|
370
|
+
s,
|
|
371
|
+
makeCtx(`queued-${randomUUID()}`) as any,
|
|
372
|
+
pi as any,
|
|
373
|
+
base,
|
|
374
|
+
false,
|
|
375
|
+
false,
|
|
376
|
+
{
|
|
377
|
+
shouldUseWorktreeIsolation: () => false,
|
|
378
|
+
registerSigtermHandler: () => {},
|
|
379
|
+
lockBase: () => base,
|
|
380
|
+
buildResolver: () => ({}) as any,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
classification: "none",
|
|
384
|
+
lock: null,
|
|
385
|
+
pausedSession: null,
|
|
386
|
+
state: null,
|
|
387
|
+
recovery: null,
|
|
388
|
+
recoveryPrompt: null,
|
|
389
|
+
recoveryToolCallCount: 0,
|
|
390
|
+
artifactSatisfied: false,
|
|
391
|
+
hasResumableDiskState: false,
|
|
392
|
+
isBootstrapCrash: false,
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
assert.equal(ready, true);
|
|
397
|
+
assert.equal(s.active, true);
|
|
398
|
+
assert.equal(s.currentMilestoneId, "M002");
|
|
399
|
+
assert.equal(messages.length, 0, "queued deep milestone must not re-enter smart new-milestone discussion");
|
|
400
|
+
} finally {
|
|
401
|
+
try { closeDatabase(); } catch {}
|
|
402
|
+
rmSync(base, { recursive: true, force: true });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
345
406
|
test("deep project setup: pre-dispatch takes precedence over an existing draft milestone", async () => {
|
|
346
407
|
const base = makeBase();
|
|
347
408
|
try {
|
|
@@ -1020,7 +1081,7 @@ test("deep project setup: research-project blocker placeholder is a file, not th
|
|
|
1020
1081
|
const base = makeBase();
|
|
1021
1082
|
try {
|
|
1022
1083
|
const expectedPath = resolveExpectedArtifactPath("research-project", "PROJECT-RESEARCH", base);
|
|
1023
|
-
assert.equal(expectedPath, join(base, ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
|
|
1084
|
+
assert.equal(expectedPath, join(realpathSync(base), ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
|
|
1024
1085
|
|
|
1025
1086
|
mkdirSync(join(base, ".gsd", "research"), { recursive: true });
|
|
1026
1087
|
const diagnosis = writeBlockerPlaceholder(
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// gsd-2 / execute-summary-save PROJECT registration hard-fail tests
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import { openDatabase, closeDatabase, getAllMilestones } from "../gsd-db.ts";
|
|
10
|
+
import { markApprovalGateVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
|
|
11
|
+
import { executeSummarySave } from "../tools/workflow-tool-executors.ts";
|
|
12
|
+
|
|
13
|
+
function makeTmpBase(): string {
|
|
14
|
+
const base = join(tmpdir(), `gsd-summary-save-empty-project-${randomUUID()}`);
|
|
15
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
16
|
+
return base;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cleanup(base: string): void {
|
|
20
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* swallow */ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function openTestDb(base: string): void {
|
|
24
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function inProjectDir<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
28
|
+
const originalCwd = process.cwd();
|
|
29
|
+
try {
|
|
30
|
+
process.chdir(dir);
|
|
31
|
+
return await fn();
|
|
32
|
+
} finally {
|
|
33
|
+
process.chdir(originalCwd);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setupBase(t: { after: (fn: () => void) => void }): string {
|
|
38
|
+
const base = makeTmpBase();
|
|
39
|
+
// Force deep planning so the root-artifact guard requires a verified approval gate,
|
|
40
|
+
// matching the production flow that surfaces the regression.
|
|
41
|
+
writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
|
|
42
|
+
openTestDb(base);
|
|
43
|
+
markApprovalGateVerified("depth_verification_project_confirm", base);
|
|
44
|
+
t.after(() => {
|
|
45
|
+
clearDiscussionFlowState(base);
|
|
46
|
+
closeDatabase();
|
|
47
|
+
cleanup(base);
|
|
48
|
+
});
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("executeSummarySave returns isError when PROJECT.md content has zero parseable milestone lines", async (t) => {
|
|
53
|
+
const base = setupBase(t);
|
|
54
|
+
|
|
55
|
+
const content = [
|
|
56
|
+
"# Project",
|
|
57
|
+
"",
|
|
58
|
+
"## What This Is",
|
|
59
|
+
"",
|
|
60
|
+
"Bad-separator regression fixture.",
|
|
61
|
+
"",
|
|
62
|
+
"## Milestone Sequence",
|
|
63
|
+
"",
|
|
64
|
+
// Wrong separator: " : " instead of em-dash / -- / - → MILESTONE_LINE_RE matches zero lines.
|
|
65
|
+
"- [ ] M001: Foundation : Establish the first runnable slice.",
|
|
66
|
+
"",
|
|
67
|
+
"## Next Section",
|
|
68
|
+
"",
|
|
69
|
+
"Trailing prose with no list bullets so MILESTONE_LINE_RE cannot bridge across lines.",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
|
|
73
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
74
|
+
artifact_type: "PROJECT",
|
|
75
|
+
content,
|
|
76
|
+
}, base));
|
|
77
|
+
|
|
78
|
+
assert.equal(result.isError, true);
|
|
79
|
+
assert.equal(result.details.error, "milestone_registration_empty_parse");
|
|
80
|
+
assert.match(result.content[0].text, /zero parseable milestone lines/);
|
|
81
|
+
assert.equal(getAllMilestones().length, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("executeSummarySave registers milestones when PROJECT.md uses canonical em-dash format", async (t) => {
|
|
85
|
+
const base = setupBase(t);
|
|
86
|
+
|
|
87
|
+
const content = [
|
|
88
|
+
"# Project",
|
|
89
|
+
"",
|
|
90
|
+
"## What This Is",
|
|
91
|
+
"",
|
|
92
|
+
"Canonical milestone-sequence fixture.",
|
|
93
|
+
"",
|
|
94
|
+
"## Milestone Sequence",
|
|
95
|
+
"",
|
|
96
|
+
"- [ ] M001: Foo — bar",
|
|
97
|
+
"- [ ] M002: Baz — qux",
|
|
98
|
+
"",
|
|
99
|
+
].join("\n");
|
|
100
|
+
|
|
101
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
102
|
+
artifact_type: "PROJECT",
|
|
103
|
+
content,
|
|
104
|
+
}, base));
|
|
105
|
+
|
|
106
|
+
assert.notEqual(result.isError, true);
|
|
107
|
+
assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
|
|
108
|
+
assert.equal(getAllMilestones().length, 2);
|
|
109
|
+
});
|
|
@@ -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', () => {
|
|
@@ -101,6 +101,20 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
|
|
|
101
101
|
prompt.includes("Implement the thing"),
|
|
102
102
|
"must include task plan body content from disk",
|
|
103
103
|
);
|
|
104
|
+
assert.ok(prompt.includes("## Context Mode"), "execute-task should include standalone Context Mode guidance");
|
|
105
|
+
assert.ok(prompt.includes("execution lane"), "execute-task should render the execution lane");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("buildExecuteTaskPrompt omits Context Mode when disabled", async () => {
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
111
|
+
["---", "context_mode:", " enabled: false", "---", ""].join("\n"),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const prompt = await buildExecuteTaskPrompt(MID, SID, S_TITLE, TID, T_TITLE, base);
|
|
115
|
+
|
|
116
|
+
assert.ok(!prompt.includes("## Context Mode"));
|
|
117
|
+
assert.ok(!prompt.includes("Context Mode (execution lane)"));
|
|
104
118
|
});
|
|
105
119
|
|
|
106
120
|
test("buildCompleteSlicePrompt carries the complete-slice contract", async () => {
|
|
@@ -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));
|
|
@@ -399,6 +399,140 @@ test("runDispatch pauses when complete-milestone summary exists on disk but the
|
|
|
399
399
|
assert.equal(stopCalls, 0, "mismatch pause should not hard-stop the loop");
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
test("runDispatch clears stuck state after Level 1 artifact recovery", async (t) => {
|
|
403
|
+
const capture = createEventCapture();
|
|
404
|
+
let invalidateCalls = 0;
|
|
405
|
+
let stopCalls = 0;
|
|
406
|
+
const base = join(tmpdir(), `gsd-stuck-plan-${randomUUID()}`);
|
|
407
|
+
t.after(() => {
|
|
408
|
+
closeDatabase();
|
|
409
|
+
rmSync(base, { recursive: true, force: true });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
413
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
414
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
415
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
416
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
417
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
418
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
419
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01\n\n## Tasks\n\n- [ ] **T01: First task** `est:1h`\n");
|
|
420
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
421
|
+
|
|
422
|
+
const deps = makeMockDeps(capture, {
|
|
423
|
+
invalidateAllCaches: () => { invalidateCalls++; },
|
|
424
|
+
stopAuto: async () => { stopCalls++; },
|
|
425
|
+
resolveDispatch: async () => ({
|
|
426
|
+
action: "dispatch" as const,
|
|
427
|
+
unitType: "plan-slice",
|
|
428
|
+
unitId: "M001/S01",
|
|
429
|
+
prompt: "plan the slice",
|
|
430
|
+
matchedRule: "planning → plan-slice",
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
const ic = makeIC(deps, {
|
|
434
|
+
s: {
|
|
435
|
+
...makeSession(),
|
|
436
|
+
basePath: base,
|
|
437
|
+
originalBasePath: base,
|
|
438
|
+
} as any,
|
|
439
|
+
});
|
|
440
|
+
const preData: PreDispatchData = {
|
|
441
|
+
state: {
|
|
442
|
+
phase: "planning",
|
|
443
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
444
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
445
|
+
registry: [{ id: "M001", status: "active" }],
|
|
446
|
+
blockers: [],
|
|
447
|
+
} as any,
|
|
448
|
+
mid: "M001",
|
|
449
|
+
midTitle: "Test Milestone",
|
|
450
|
+
};
|
|
451
|
+
const loopState: LoopState = {
|
|
452
|
+
recentUnits: [
|
|
453
|
+
{ key: "plan-slice/M001/S01" },
|
|
454
|
+
{ key: "plan-slice/M001/S01" },
|
|
455
|
+
],
|
|
456
|
+
stuckRecoveryAttempts: 0,
|
|
457
|
+
consecutiveFinalizeTimeouts: 0,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = await runDispatch(ic, preData, loopState);
|
|
461
|
+
|
|
462
|
+
assert.equal(result.action, "continue");
|
|
463
|
+
assert.equal(invalidateCalls, 1, "Level 1 artifact recovery should invalidate caches");
|
|
464
|
+
assert.equal(stopCalls, 0, "Level 1 artifact recovery should not hard-stop");
|
|
465
|
+
assert.deepEqual(loopState.recentUnits, [], "Level 1 artifact recovery should clear the stuck window");
|
|
466
|
+
assert.equal(loopState.stuckRecoveryAttempts, 0, "Level 1 artifact recovery should reset the recovery counter");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("runDispatch escapes Level 2 stuck stop when artifact verifies after cache invalidation", async (t) => {
|
|
470
|
+
const capture = createEventCapture();
|
|
471
|
+
let invalidateCalls = 0;
|
|
472
|
+
let stopCalls = 0;
|
|
473
|
+
const base = join(tmpdir(), `gsd-stuck-plan-l2-${randomUUID()}`);
|
|
474
|
+
t.after(() => {
|
|
475
|
+
closeDatabase();
|
|
476
|
+
rmSync(base, { recursive: true, force: true });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
480
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
481
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
482
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
483
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
484
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
485
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
486
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01\n\n## Tasks\n\n- [ ] **T01: First task** `est:1h`\n");
|
|
487
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
488
|
+
|
|
489
|
+
const deps = makeMockDeps(capture, {
|
|
490
|
+
invalidateAllCaches: () => { invalidateCalls++; },
|
|
491
|
+
stopAuto: async () => { stopCalls++; },
|
|
492
|
+
resolveDispatch: async () => ({
|
|
493
|
+
action: "dispatch" as const,
|
|
494
|
+
unitType: "plan-slice",
|
|
495
|
+
unitId: "M001/S01",
|
|
496
|
+
prompt: "plan the slice",
|
|
497
|
+
matchedRule: "planning → plan-slice",
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
const ic = makeIC(deps, {
|
|
501
|
+
s: {
|
|
502
|
+
...makeSession(),
|
|
503
|
+
basePath: base,
|
|
504
|
+
originalBasePath: base,
|
|
505
|
+
} as any,
|
|
506
|
+
});
|
|
507
|
+
const preData: PreDispatchData = {
|
|
508
|
+
state: {
|
|
509
|
+
phase: "planning",
|
|
510
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
511
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
512
|
+
registry: [{ id: "M001", status: "active" }],
|
|
513
|
+
blockers: [],
|
|
514
|
+
} as any,
|
|
515
|
+
mid: "M001",
|
|
516
|
+
midTitle: "Test Milestone",
|
|
517
|
+
};
|
|
518
|
+
const loopState: LoopState = {
|
|
519
|
+
recentUnits: [
|
|
520
|
+
{ key: "plan-slice/M001/S01" },
|
|
521
|
+
{ key: "plan-slice/M001/S01" },
|
|
522
|
+
],
|
|
523
|
+
stuckRecoveryAttempts: 1,
|
|
524
|
+
consecutiveFinalizeTimeouts: 0,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const result = await runDispatch(ic, preData, loopState);
|
|
528
|
+
|
|
529
|
+
assert.equal(result.action, "continue");
|
|
530
|
+
assert.equal(invalidateCalls, 1, "Level 2 escape should invalidate caches before rechecking artifacts");
|
|
531
|
+
assert.equal(stopCalls, 0, "verified artifacts should escape Level 2 hard stop");
|
|
532
|
+
assert.deepEqual(loopState.recentUnits, [], "Level 2 artifact escape should clear the stuck window");
|
|
533
|
+
assert.equal(loopState.stuckRecoveryAttempts, 0, "Level 2 artifact escape should reset the recovery counter");
|
|
534
|
+
});
|
|
535
|
+
|
|
402
536
|
test("runUnitPhase emits unit-start and unit-end with causedBy reference", async () => {
|
|
403
537
|
const capture = createEventCapture();
|
|
404
538
|
|
|
@@ -147,4 +147,12 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
|
|
|
147
147
|
prompt.includes(SKILL_ACTIVATION_SUBSTRING),
|
|
148
148
|
`parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
|
|
149
149
|
);
|
|
150
|
+
assert.ok(
|
|
151
|
+
prompt.includes("Context Mode (research lane):"),
|
|
152
|
+
"embedded parallel research subagent prompts should use nested Context Mode guidance",
|
|
153
|
+
);
|
|
154
|
+
assert.ok(
|
|
155
|
+
!prompt.includes("## Context Mode\n\nLane: **research lane**."),
|
|
156
|
+
"embedded parallel research subagent prompts should not use standalone Context Mode heading",
|
|
157
|
+
);
|
|
150
158
|
});
|
|
@@ -64,6 +64,7 @@ test("readPausedSessionMetadata round-trips a real PausedSessionMetadata payload
|
|
|
64
64
|
activeRunDir: null,
|
|
65
65
|
autoStartTime: Date.now(),
|
|
66
66
|
milestoneLock: null,
|
|
67
|
+
pauseReason: "Blocked: waiting for UAT",
|
|
67
68
|
};
|
|
68
69
|
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
|
|
69
70
|
|
|
@@ -73,6 +74,7 @@ test("readPausedSessionMetadata round-trips a real PausedSessionMetadata payload
|
|
|
73
74
|
assert.equal(loaded!.unitType, "plan-slice");
|
|
74
75
|
assert.equal(loaded!.unitId, "M001/S01");
|
|
75
76
|
assert.equal(loaded!.sessionFile, "/tmp/session.jsonl");
|
|
77
|
+
assert.equal(loaded!.pauseReason, "Blocked: waiting for UAT");
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test("readPausedSessionMetadata auto-deletes stale pseudo-milestone pause rows", (t) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD Extension — Plan-slice tool integration tests.
|
|
2
|
+
|
|
1
3
|
import test from 'node:test';
|
|
2
4
|
import assert from 'node:assert/strict';
|
|
3
5
|
import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
@@ -8,6 +10,7 @@ import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, ge
|
|
|
8
10
|
import { handlePlanSlice } from '../tools/plan-slice.ts';
|
|
9
11
|
import { parsePlan } from '../parsers-legacy.ts';
|
|
10
12
|
import { parseTaskPlanFile } from '../files.ts';
|
|
13
|
+
import { deriveState, invalidateStateCache } from '../state.ts';
|
|
11
14
|
|
|
12
15
|
function makeTmpBase(): string {
|
|
13
16
|
const base = mkdtempSync(join(tmpdir(), 'gsd-plan-slice-'));
|
|
@@ -98,6 +101,30 @@ test('handlePlanSlice writes slice/task planning state and renders plan artifact
|
|
|
98
101
|
}
|
|
99
102
|
});
|
|
100
103
|
|
|
104
|
+
test('handlePlanSlice advances DB-derived state out of planning immediately', async () => {
|
|
105
|
+
const base = makeTmpBase();
|
|
106
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
seedParentSlice();
|
|
110
|
+
|
|
111
|
+
invalidateStateCache();
|
|
112
|
+
const before = await deriveState(base);
|
|
113
|
+
assert.equal(before.phase, 'planning');
|
|
114
|
+
assert.equal(before.progress?.tasks?.total, 0);
|
|
115
|
+
|
|
116
|
+
const result = await handlePlanSlice(validParams(), base);
|
|
117
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
118
|
+
|
|
119
|
+
invalidateStateCache();
|
|
120
|
+
const after = await deriveState(base);
|
|
121
|
+
assert.notEqual(after.phase, 'planning');
|
|
122
|
+
assert.equal(after.progress?.tasks?.total, 2);
|
|
123
|
+
} finally {
|
|
124
|
+
cleanup(base);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
101
128
|
test('handlePlanSlice leaves omitted enrichment fields empty instead of rendering placeholders', async () => {
|
|
102
129
|
const base = makeTmpBase();
|
|
103
130
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|