gsd-pi 2.67.0-dev.2142d3e → 2.67.0-dev.2367d7e
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 +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
- package/dist/resources/extensions/gsd/auto/session.js +10 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +16 -30
- package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
- package/dist/resources/extensions/gsd/auto.js +121 -59
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
- package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
- package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
- package/dist/resources/extensions/gsd/doctor.js +8 -4
- package/dist/resources/extensions/gsd/gsd-db.js +11 -0
- package/dist/resources/extensions/gsd/guided-flow.js +40 -31
- package/dist/resources/extensions/gsd/init-wizard.js +15 -12
- package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
- package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
- package/dist/resources/extensions/gsd/state.js +7 -2
- package/dist/resources/extensions/gsd/workflow-mcp.js +90 -19
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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/react-loadable-manifest.json +2 -2
- 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_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 +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 +2 -2
- 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 +2 -2
- 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/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-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/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
- package/package.json +4 -2
- package/packages/mcp-server/dist/cli.d.ts +9 -0
- package/packages/mcp-server/dist/cli.d.ts.map +1 -0
- package/packages/mcp-server/dist/cli.js +58 -0
- package/packages/mcp-server/dist/cli.js.map +1 -0
- package/packages/mcp-server/dist/index.d.ts +20 -0
- package/packages/mcp-server/dist/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/index.js +14 -0
- package/packages/mcp-server/dist/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
- package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/captures.js +67 -0
- package/packages/mcp-server/dist/readers/captures.js.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
- package/packages/mcp-server/dist/readers/index.d.ts +14 -0
- package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/index.js +10 -0
- package/packages/mcp-server/dist/readers/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.js +82 -0
- package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.js +74 -0
- package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
- package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
- package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/paths.js +199 -0
- package/packages/mcp-server/dist/readers/paths.js.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.js +194 -0
- package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
- package/packages/mcp-server/dist/readers/state.d.ts +43 -0
- package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/state.js +184 -0
- package/packages/mcp-server/dist/readers/state.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -0
- package/packages/mcp-server/dist/server.js +319 -0
- package/packages/mcp-server/dist/server.js.map +1 -0
- package/packages/mcp-server/dist/session-manager.d.ts +54 -0
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
- package/packages/mcp-server/dist/session-manager.js +284 -0
- package/packages/mcp-server/dist/session-manager.js.map +1 -0
- package/packages/mcp-server/dist/types.d.ts +61 -0
- package/packages/mcp-server/dist/types.d.ts.map +1 -0
- package/packages/mcp-server/dist/types.js +11 -0
- package/packages/mcp-server/dist/types.js.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.js +532 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
- package/packages/mcp-server/src/workflow-tools.ts +13 -2
- package/packages/mcp-server/tsconfig.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +14 -6
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
- package/packages/pi-agent-core/src/agent-loop.ts +20 -6
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
- package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
- package/packages/pi-coding-agent/src/core/index.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
- package/packages/rpc-client/dist/index.d.ts +10 -0
- package/packages/rpc-client/dist/index.d.ts.map +1 -0
- package/packages/rpc-client/dist/index.js +9 -0
- package/packages/rpc-client/dist/index.js.map +1 -0
- package/packages/rpc-client/dist/jsonl.d.ts +17 -0
- package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
- package/packages/rpc-client/dist/jsonl.js +54 -0
- package/packages/rpc-client/dist/jsonl.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
- package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.js +541 -0
- package/packages/rpc-client/dist/rpc-client.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.js +477 -0
- package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
- package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
- package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-types.js +12 -0
- package/packages/rpc-client/dist/rpc-types.js.map +1 -0
- package/scripts/ensure-workspace-builds.cjs +2 -0
- package/scripts/link-workspace-packages.cjs +21 -14
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
- package/src/resources/extensions/gsd/auto/session.ts +10 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +23 -55
- package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
- package/src/resources/extensions/gsd/auto.ts +133 -64
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
- package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
- package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
- package/src/resources/extensions/gsd/doctor.ts +9 -5
- package/src/resources/extensions/gsd/gsd-db.ts +12 -0
- package/src/resources/extensions/gsd/guided-flow.ts +42 -36
- package/src/resources/extensions/gsd/init-wizard.ts +17 -11
- package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
- package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
- package/src/resources/extensions/gsd/state.ts +7 -1
- package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
- package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
- package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
- package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +212 -13
- package/src/resources/extensions/gsd/workflow-mcp.ts +106 -19
- package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
- package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
- /package/dist/web/standalone/.next/static/{xR6qurkuYSvyjBjRyJLxG → WMDT_0C0XDkBKtsAI_AX4}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{xR6qurkuYSvyjBjRyJLxG → WMDT_0C0XDkBKtsAI_AX4}/_ssgManifest.js +0 -0
|
@@ -18,22 +18,33 @@ const { assertTrue, report } = createTestContext();
|
|
|
18
18
|
|
|
19
19
|
const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
|
|
20
20
|
|
|
21
|
-
console.log("\n===
|
|
21
|
+
console.log("\n=== resume path refreshes resources and opens DB before rebuildState/deriveState ===");
|
|
22
22
|
|
|
23
23
|
// The resume block is the `if (s.paused) { ... }` section that calls rebuildState/deriveState.
|
|
24
24
|
// Locate the resume section by finding `s.paused = false;` followed by `rebuildState`.
|
|
25
25
|
const resumeSectionStart = autoSrc.indexOf("if (s.paused) {", autoSrc.indexOf("// If resuming from paused state"));
|
|
26
26
|
assertTrue(resumeSectionStart > 0, "auto.ts has the paused-session resume block");
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const resumeSectionEnd = autoSrc.indexOf("await autoLoop(", resumeSectionStart);
|
|
29
|
+
assertTrue(resumeSectionEnd > resumeSectionStart, "resume block reaches autoLoop");
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
const resumeSection = autoSrc.slice(resumeSectionStart, resumeSectionEnd);
|
|
32
|
+
|
|
33
|
+
// The resume path must refresh managed resources and open the DB before
|
|
34
|
+
// rebuildState/deriveState so resumed auto-mode uses current extension code.
|
|
31
35
|
const rebuildIdx = resumeSection.indexOf("rebuildState(");
|
|
32
36
|
assertTrue(rebuildIdx > 0, "resume block calls rebuildState");
|
|
33
37
|
|
|
34
38
|
const deriveIdx = resumeSection.indexOf("deriveState(");
|
|
35
39
|
assertTrue(deriveIdx > 0, "resume block calls deriveState");
|
|
36
40
|
|
|
41
|
+
const preDeriveSection = resumeSection.slice(0, rebuildIdx);
|
|
42
|
+
|
|
43
|
+
assertTrue(
|
|
44
|
+
preDeriveSection.includes("initResources("),
|
|
45
|
+
"resume path must refresh managed resources before rebuildState/deriveState (#3761)",
|
|
46
|
+
);
|
|
47
|
+
|
|
37
48
|
// There must be a DB open call before the first rebuildState call
|
|
38
49
|
const dbOpenPatterns = [
|
|
39
50
|
"openProjectDbIfPresent(",
|
|
@@ -41,7 +52,6 @@ const dbOpenPatterns = [
|
|
|
41
52
|
"ensureDbOpen(",
|
|
42
53
|
];
|
|
43
54
|
|
|
44
|
-
const preDeriveSection = resumeSection.slice(0, rebuildIdx);
|
|
45
55
|
const hasDbOpen = dbOpenPatterns.some(pat => preDeriveSection.includes(pat));
|
|
46
56
|
assertTrue(
|
|
47
57
|
hasDbOpen,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
test("copyPlanningArtifacts skips when source and destination .gsd resolve to the same path", () => {
|
|
7
|
+
const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
8
|
+
const src = readFileSync(srcPath, "utf-8");
|
|
9
|
+
|
|
10
|
+
const fnIdx = src.indexOf("function copyPlanningArtifacts");
|
|
11
|
+
assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
|
|
12
|
+
|
|
13
|
+
const fnBody = src.slice(fnIdx, fnIdx + 2400);
|
|
14
|
+
|
|
15
|
+
const guardIdx = fnBody.indexOf("if (isSamePath(srcGsd, dstGsd)) return;");
|
|
16
|
+
const copyIdx = fnBody.indexOf("safeCopyRecursive(join(srcGsd, \"milestones\")");
|
|
17
|
+
|
|
18
|
+
assert.ok(guardIdx !== -1, "copyPlanningArtifacts should guard same-path .gsd copies");
|
|
19
|
+
assert.ok(copyIdx !== -1, "copyPlanningArtifacts should still copy milestones when paths differ");
|
|
20
|
+
assert.ok(guardIdx < copyIdx, "same-path guard should run before any copy attempt");
|
|
21
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync,
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -13,6 +13,14 @@ import {
|
|
|
13
13
|
formatCrashInfo,
|
|
14
14
|
type LockData,
|
|
15
15
|
} from "../crash-recovery.ts";
|
|
16
|
+
import {
|
|
17
|
+
assessInterruptedSession,
|
|
18
|
+
hasResumableDerivedState,
|
|
19
|
+
isBootstrapCrashLock,
|
|
20
|
+
readPausedSessionMetadata,
|
|
21
|
+
} from "../interrupted-session.ts";
|
|
22
|
+
import { gsdRoot } from "../paths.ts";
|
|
23
|
+
import type { GSDState } from "../types.ts";
|
|
16
24
|
|
|
17
25
|
function makeTmpBase(): string {
|
|
18
26
|
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
@@ -24,6 +32,376 @@ function cleanup(base: string): void {
|
|
|
24
32
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
function writeTestLock(
|
|
36
|
+
base: string,
|
|
37
|
+
unitType: string,
|
|
38
|
+
unitId: string,
|
|
39
|
+
sessionFile?: string,
|
|
40
|
+
): void {
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(gsdRoot(base), "auto.lock"),
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
pid: 999999999,
|
|
45
|
+
startedAt: new Date().toISOString(),
|
|
46
|
+
unitType,
|
|
47
|
+
unitId,
|
|
48
|
+
unitStartedAt: new Date().toISOString(),
|
|
49
|
+
sessionFile,
|
|
50
|
+
}, null, 2),
|
|
51
|
+
"utf-8",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeRoadmap(base: string, checked = false): void {
|
|
56
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
57
|
+
mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(milestoneDir, "M001-ROADMAP.md"),
|
|
60
|
+
[
|
|
61
|
+
"# M001: Test Milestone",
|
|
62
|
+
"",
|
|
63
|
+
"## Vision",
|
|
64
|
+
"",
|
|
65
|
+
"Test milestone.",
|
|
66
|
+
"",
|
|
67
|
+
"## Success Criteria",
|
|
68
|
+
"",
|
|
69
|
+
"- It works.",
|
|
70
|
+
"",
|
|
71
|
+
"## Slices",
|
|
72
|
+
"",
|
|
73
|
+
`- [${checked ? "x" : " "}] **S01: Test slice** \`risk:low\``,
|
|
74
|
+
" After this: Demo",
|
|
75
|
+
"",
|
|
76
|
+
"## Boundary Map",
|
|
77
|
+
"",
|
|
78
|
+
"- S01 → terminal",
|
|
79
|
+
" - Produces: done",
|
|
80
|
+
" - Consumes: nothing",
|
|
81
|
+
].join("\n"),
|
|
82
|
+
"utf-8",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeCompleteSliceArtifacts(base: string): void {
|
|
87
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
88
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
89
|
+
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
|
|
90
|
+
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeCompleteMilestoneSummary(base: string): void {
|
|
94
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
95
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
96
|
+
writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function writePausedSession(
|
|
100
|
+
base: string,
|
|
101
|
+
milestoneId = "M001",
|
|
102
|
+
stepMode = false,
|
|
103
|
+
worktreePath?: string,
|
|
104
|
+
unitType?: string,
|
|
105
|
+
unitId?: string,
|
|
106
|
+
): void {
|
|
107
|
+
const runtimeDir = join(base, ".gsd", "runtime");
|
|
108
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(runtimeDir, "paused-session.json"),
|
|
111
|
+
JSON.stringify({ milestoneId, originalBasePath: base, stepMode, worktreePath, unitType, unitId }, null, 2),
|
|
112
|
+
"utf-8",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeActivityLog(base: string, entries: Record<string, unknown>[]): void {
|
|
117
|
+
const activityDir = join(base, ".gsd", "activity");
|
|
118
|
+
mkdirSync(activityDir, { recursive: true });
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(activityDir, "001-execute-task-M001-S01-T01.jsonl"),
|
|
121
|
+
entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
122
|
+
"utf-8",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function makeState(phase: GSDState["phase"], activeMilestone = true): GSDState {
|
|
127
|
+
return {
|
|
128
|
+
activeMilestone: activeMilestone ? { id: "M001", title: "Test" } : null,
|
|
129
|
+
activeSlice: null,
|
|
130
|
+
activeTask: null,
|
|
131
|
+
phase,
|
|
132
|
+
recentDecisions: [],
|
|
133
|
+
blockers: [],
|
|
134
|
+
nextAction: "",
|
|
135
|
+
registry: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── interrupted-session helpers ───────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
test("hasResumableDerivedState treats only unfinished active work as resumable", () => {
|
|
142
|
+
assert.equal(hasResumableDerivedState(makeState("executing")), true);
|
|
143
|
+
assert.equal(hasResumableDerivedState(makeState("complete")), false);
|
|
144
|
+
assert.equal(hasResumableDerivedState(makeState("pre-planning", false)), false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("isBootstrapCrashLock detects starting/bootstrap special case", () => {
|
|
148
|
+
const bootstrap: LockData = {
|
|
149
|
+
pid: 999999999,
|
|
150
|
+
startedAt: new Date().toISOString(),
|
|
151
|
+
unitType: "starting",
|
|
152
|
+
unitId: "bootstrap",
|
|
153
|
+
unitStartedAt: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
assert.equal(isBootstrapCrashLock(bootstrap), true);
|
|
156
|
+
assert.equal(isBootstrapCrashLock({ ...bootstrap, unitType: "execute-task" }), false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("readPausedSessionMetadata reads paused-session metadata when present", () => {
|
|
160
|
+
const base = makeTmpBase();
|
|
161
|
+
try {
|
|
162
|
+
writePausedSession(base, "M009");
|
|
163
|
+
const meta = readPausedSessionMetadata(base);
|
|
164
|
+
assert.equal(meta?.milestoneId, "M009");
|
|
165
|
+
} finally {
|
|
166
|
+
cleanup(base);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("readPausedSessionMetadata preserves unitType and unitId through round-trip", () => {
|
|
171
|
+
const base = makeTmpBase();
|
|
172
|
+
try {
|
|
173
|
+
writePausedSession(base, "M001", false, undefined, "execute-task", "M001/S01/T02");
|
|
174
|
+
const meta = readPausedSessionMetadata(base);
|
|
175
|
+
assert.equal(meta?.unitType, "execute-task");
|
|
176
|
+
assert.equal(meta?.unitId, "M001/S01/T02");
|
|
177
|
+
} finally {
|
|
178
|
+
cleanup(base);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("readPausedSessionMetadata handles legacy metadata without unitType/unitId", () => {
|
|
183
|
+
const base = makeTmpBase();
|
|
184
|
+
try {
|
|
185
|
+
// Write metadata without unitType/unitId (simulates older version)
|
|
186
|
+
const runtimeDir = join(base, ".gsd", "runtime");
|
|
187
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
188
|
+
writeFileSync(
|
|
189
|
+
join(runtimeDir, "paused-session.json"),
|
|
190
|
+
JSON.stringify({ milestoneId: "M001", originalBasePath: base }),
|
|
191
|
+
"utf-8",
|
|
192
|
+
);
|
|
193
|
+
const meta = readPausedSessionMetadata(base);
|
|
194
|
+
assert.equal(meta?.milestoneId, "M001");
|
|
195
|
+
assert.equal(meta?.unitType, undefined);
|
|
196
|
+
assert.equal(meta?.unitId, undefined);
|
|
197
|
+
} finally {
|
|
198
|
+
cleanup(base);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("assessInterruptedSession returns none when no lock and no paused session exist", async () => {
|
|
203
|
+
const base = makeTmpBase();
|
|
204
|
+
try {
|
|
205
|
+
const assessment = await assessInterruptedSession(base);
|
|
206
|
+
assert.equal(assessment.classification, "none");
|
|
207
|
+
assert.equal(assessment.lock, null);
|
|
208
|
+
assert.equal(assessment.pausedSession, null);
|
|
209
|
+
assert.equal(assessment.state, null);
|
|
210
|
+
assert.equal(assessment.recovery, null);
|
|
211
|
+
assert.equal(assessment.recoveryPrompt, null);
|
|
212
|
+
assert.equal(assessment.recoveryToolCallCount, 0);
|
|
213
|
+
assert.equal(assessment.artifactSatisfied, false);
|
|
214
|
+
assert.equal(assessment.hasResumableDiskState, false);
|
|
215
|
+
assert.equal(assessment.isBootstrapCrash, false);
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup(base);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("assessInterruptedSession classifies stale complete repo as stale and suppresses recovery", async () => {
|
|
222
|
+
const base = makeTmpBase();
|
|
223
|
+
try {
|
|
224
|
+
writeRoadmap(base, true);
|
|
225
|
+
writeCompleteSliceArtifacts(base);
|
|
226
|
+
writeCompleteMilestoneSummary(base);
|
|
227
|
+
writeTestLock(base, "execute-task", "M001/S01/T01");
|
|
228
|
+
|
|
229
|
+
const assessment = await assessInterruptedSession(base);
|
|
230
|
+
assert.equal(assessment.classification, "stale");
|
|
231
|
+
assert.equal(assessment.hasResumableDiskState, false);
|
|
232
|
+
assert.equal(assessment.recoveryPrompt, null);
|
|
233
|
+
} finally {
|
|
234
|
+
cleanup(base);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("assessInterruptedSession suppresses prompt when expected artifact already exists and no resumable state remains", async () => {
|
|
239
|
+
const base = makeTmpBase();
|
|
240
|
+
try {
|
|
241
|
+
writeRoadmap(base, true);
|
|
242
|
+
writeCompleteSliceArtifacts(base);
|
|
243
|
+
writeCompleteMilestoneSummary(base);
|
|
244
|
+
writeTestLock(base, "complete-slice", "M001/S01");
|
|
245
|
+
|
|
246
|
+
const assessment = await assessInterruptedSession(base);
|
|
247
|
+
assert.equal(assessment.classification, "stale");
|
|
248
|
+
assert.equal(assessment.artifactSatisfied, true);
|
|
249
|
+
} finally {
|
|
250
|
+
cleanup(base);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("assessInterruptedSession keeps paused-session resume recoverable when disk state is unfinished", async () => {
|
|
255
|
+
const base = makeTmpBase();
|
|
256
|
+
try {
|
|
257
|
+
writeRoadmap(base, false);
|
|
258
|
+
writePausedSession(base);
|
|
259
|
+
writeTestLock(base, "execute-task", "M001/S01/T01");
|
|
260
|
+
|
|
261
|
+
const assessment = await assessInterruptedSession(base);
|
|
262
|
+
assert.equal(assessment.classification, "recoverable");
|
|
263
|
+
assert.equal(assessment.pausedSession?.milestoneId, "M001");
|
|
264
|
+
} finally {
|
|
265
|
+
cleanup(base);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("assessInterruptedSession marks stale paused-session metadata as stale when no work remains", async () => {
|
|
270
|
+
const base = makeTmpBase();
|
|
271
|
+
try {
|
|
272
|
+
writeRoadmap(base, true);
|
|
273
|
+
writeCompleteSliceArtifacts(base);
|
|
274
|
+
writeCompleteMilestoneSummary(base);
|
|
275
|
+
writePausedSession(base, "M999");
|
|
276
|
+
|
|
277
|
+
const assessment = await assessInterruptedSession(base);
|
|
278
|
+
assert.equal(assessment.classification, "stale");
|
|
279
|
+
assert.equal(assessment.hasResumableDiskState, false);
|
|
280
|
+
} finally {
|
|
281
|
+
cleanup(base);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("assessInterruptedSession classifies paused session without lock as recoverable when disk state is resumable", async () => {
|
|
286
|
+
const base = makeTmpBase();
|
|
287
|
+
try {
|
|
288
|
+
writeRoadmap(base, false);
|
|
289
|
+
writePausedSession(base, "M001", true);
|
|
290
|
+
|
|
291
|
+
const assessment = await assessInterruptedSession(base);
|
|
292
|
+
assert.equal(assessment.classification, "recoverable");
|
|
293
|
+
assert.equal(assessment.lock, null);
|
|
294
|
+
assert.equal(assessment.pausedSession?.milestoneId, "M001");
|
|
295
|
+
assert.equal(assessment.hasResumableDiskState, true);
|
|
296
|
+
assert.equal(assessment.isBootstrapCrash, false);
|
|
297
|
+
} finally {
|
|
298
|
+
cleanup(base);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("assessInterruptedSession falls back to basePath when worktreePath no longer exists", async () => {
|
|
303
|
+
const base = makeTmpBase();
|
|
304
|
+
try {
|
|
305
|
+
writeRoadmap(base, false);
|
|
306
|
+
// Reference a worktree that doesn't exist on disk
|
|
307
|
+
writePausedSession(base, "M001", false, "/nonexistent/worktree");
|
|
308
|
+
|
|
309
|
+
const assessment = await assessInterruptedSession(base);
|
|
310
|
+
// Should use basePath (which has an unfinished roadmap) instead of the missing worktree
|
|
311
|
+
assert.equal(assessment.classification, "recoverable");
|
|
312
|
+
assert.equal(assessment.hasResumableDiskState, true);
|
|
313
|
+
} finally {
|
|
314
|
+
cleanup(base);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("assessInterruptedSession prefers paused worktree state when worktreePath is recorded", async () => {
|
|
319
|
+
const base = makeTmpBase();
|
|
320
|
+
const worktree = join(base, "worktree-copy");
|
|
321
|
+
try {
|
|
322
|
+
writeRoadmap(base, false);
|
|
323
|
+
writeRoadmap(worktree, true);
|
|
324
|
+
writeCompleteSliceArtifacts(worktree);
|
|
325
|
+
writeCompleteMilestoneSummary(worktree);
|
|
326
|
+
writePausedSession(base, "M001", false, worktree);
|
|
327
|
+
|
|
328
|
+
const assessment = await assessInterruptedSession(base);
|
|
329
|
+
assert.equal(assessment.classification, "stale");
|
|
330
|
+
assert.equal(assessment.hasResumableDiskState, false);
|
|
331
|
+
} finally {
|
|
332
|
+
cleanup(base);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("assessInterruptedSession keeps unfinished derived state recoverable without trace", async () => {
|
|
337
|
+
const base = makeTmpBase();
|
|
338
|
+
try {
|
|
339
|
+
writeRoadmap(base, false);
|
|
340
|
+
writeTestLock(base, "plan-slice", "M001/S01");
|
|
341
|
+
|
|
342
|
+
const assessment = await assessInterruptedSession(base);
|
|
343
|
+
assert.equal(assessment.classification, "recoverable");
|
|
344
|
+
assert.equal(assessment.hasResumableDiskState, true);
|
|
345
|
+
assert.equal(assessment.recoveryPrompt, null);
|
|
346
|
+
} finally {
|
|
347
|
+
cleanup(base);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("assessInterruptedSession preserves crash trace when activity log has tool calls", async () => {
|
|
352
|
+
const base = makeTmpBase();
|
|
353
|
+
try {
|
|
354
|
+
writeRoadmap(base, false);
|
|
355
|
+
writeTestLock(base, "execute-task", "M001/S01/T01");
|
|
356
|
+
writeActivityLog(base, [
|
|
357
|
+
{
|
|
358
|
+
type: "message",
|
|
359
|
+
message: {
|
|
360
|
+
role: "assistant",
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "toolCall",
|
|
364
|
+
id: "1",
|
|
365
|
+
name: "bash",
|
|
366
|
+
arguments: { command: "npm test" },
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
type: "message",
|
|
373
|
+
message: {
|
|
374
|
+
role: "toolResult",
|
|
375
|
+
toolCallId: "1",
|
|
376
|
+
toolName: "bash",
|
|
377
|
+
isError: false,
|
|
378
|
+
content: [{ type: "text", text: "ok" }],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const assessment = await assessInterruptedSession(base);
|
|
384
|
+
assert.equal(assessment.classification, "recoverable");
|
|
385
|
+
assert.ok(assessment.recoveryToolCallCount > 0);
|
|
386
|
+
assert.ok(assessment.recoveryPrompt?.includes("Recovery Briefing"));
|
|
387
|
+
} finally {
|
|
388
|
+
cleanup(base);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("assessInterruptedSession treats bootstrap crash as stale without paused metadata", async () => {
|
|
393
|
+
const base = makeTmpBase();
|
|
394
|
+
try {
|
|
395
|
+
writeTestLock(base, "starting", "bootstrap");
|
|
396
|
+
|
|
397
|
+
const assessment = await assessInterruptedSession(base);
|
|
398
|
+
assert.equal(assessment.classification, "stale");
|
|
399
|
+
assert.equal(assessment.isBootstrapCrash, true);
|
|
400
|
+
} finally {
|
|
401
|
+
cleanup(base);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
27
405
|
// ─── writeLock / readCrashLock ────────────────────────────────────────────
|
|
28
406
|
|
|
29
407
|
test("writeLock creates lock file and readCrashLock reads it", (t) => {
|
|
@@ -84,7 +462,7 @@ test("#2470: isLockProcessAlive returns true for own PID (we hold the lock)", ()
|
|
|
84
462
|
|
|
85
463
|
test("isLockProcessAlive returns false for dead PID", () => {
|
|
86
464
|
const lock: LockData = {
|
|
87
|
-
pid: 999999999,
|
|
465
|
+
pid: 999999999,
|
|
88
466
|
startedAt: new Date().toISOString(),
|
|
89
467
|
unitType: "execute-task",
|
|
90
468
|
unitId: "M001/S01/T01",
|
|
@@ -126,4 +126,34 @@ describe("forensics context persistence (#2941)", () => {
|
|
|
126
126
|
// Should not throw
|
|
127
127
|
clearForensicsMarker(join(tmpBase, "nonexistent"));
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it("buildForensicsContextInjection keeps marker for low-entropy resume prompts", async () => {
|
|
131
|
+
const { buildForensicsContextInjection } = await import("../bootstrap/system-context.ts");
|
|
132
|
+
|
|
133
|
+
const markerPath = join(tmpBase, ".gsd", "runtime", "active-forensics.json");
|
|
134
|
+
writeFileSync(markerPath, JSON.stringify({
|
|
135
|
+
reportPath: "/some/report.md",
|
|
136
|
+
promptContent: "forensics prompt",
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
}), "utf-8");
|
|
139
|
+
|
|
140
|
+
const result = buildForensicsContextInjection(tmpBase, "continue");
|
|
141
|
+
assert.equal(result, "forensics prompt");
|
|
142
|
+
assert.ok(existsSync(markerPath), "resume-like follow-up should keep marker intact");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("buildForensicsContextInjection clears marker on unrelated user prompts", async () => {
|
|
146
|
+
const { buildForensicsContextInjection } = await import("../bootstrap/system-context.ts");
|
|
147
|
+
|
|
148
|
+
const markerPath = join(tmpBase, ".gsd", "runtime", "active-forensics.json");
|
|
149
|
+
writeFileSync(markerPath, JSON.stringify({
|
|
150
|
+
reportPath: "/some/report.md",
|
|
151
|
+
promptContent: "forensics prompt",
|
|
152
|
+
createdAt: new Date().toISOString(),
|
|
153
|
+
}), "utf-8");
|
|
154
|
+
|
|
155
|
+
const result = buildForensicsContextInjection(tmpBase, "please summarize the README");
|
|
156
|
+
assert.equal(result, null);
|
|
157
|
+
assert.ok(!existsSync(markerPath), "unrelated follow-up should clear the stale marker");
|
|
158
|
+
});
|
|
129
159
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
openDatabase,
|
|
8
8
|
closeDatabase,
|
|
9
9
|
isDbAvailable,
|
|
10
|
+
wasDbOpenAttempted,
|
|
10
11
|
getDbProvider,
|
|
11
12
|
insertDecision,
|
|
12
13
|
getDecisionById,
|
|
@@ -346,6 +347,17 @@ describe('gsd-db', () => {
|
|
|
346
347
|
assert.deepStrictEqual(ar, [], 'getActiveRequirements returns [] when DB closed');
|
|
347
348
|
});
|
|
348
349
|
|
|
350
|
+
test('gsd-db: wasDbOpenAttempted tracks openDatabase calls', () => {
|
|
351
|
+
// wasDbOpenAttempted should return true once openDatabase has been called
|
|
352
|
+
// (previous tests in this suite already called openDatabase, so the flag is set)
|
|
353
|
+
assert.ok(wasDbOpenAttempted(), 'wasDbOpenAttempted should be true after openDatabase was called');
|
|
354
|
+
|
|
355
|
+
// Verify the flag persists even after closeDatabase
|
|
356
|
+
closeDatabase();
|
|
357
|
+
assert.ok(!isDbAvailable(), 'DB should not be available after close');
|
|
358
|
+
assert.ok(wasDbOpenAttempted(), 'wasDbOpenAttempted should remain true after closeDatabase');
|
|
359
|
+
});
|
|
360
|
+
|
|
349
361
|
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
350
362
|
|
|
351
363
|
});
|
|
@@ -100,7 +100,7 @@ describe("#2985 Bug 4 — getDiscussionMilestoneId must be keyed by basePath", (
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test("checkAutoStartAfterDiscuss
|
|
103
|
+
test("checkAutoStartAfterDiscuss ignores missing manifest for single-milestone discuss on established project", () => {
|
|
104
104
|
const base = mkdtempSync(join(tmpdir(), "gsd-auto-start-manifest-"));
|
|
105
105
|
try {
|
|
106
106
|
const gsdDir = join(base, ".gsd");
|
|
@@ -123,7 +123,7 @@ test("checkAutoStartAfterDiscuss fails closed when a multi-milestone manifest is
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
const started = checkAutoStartAfterDiscuss();
|
|
126
|
-
assert.equal(started,
|
|
126
|
+
assert.equal(started, true, "project history alone should not require a manifest");
|
|
127
127
|
} finally {
|
|
128
128
|
clearPendingAutoStart();
|
|
129
129
|
rmSync(base, { recursive: true, force: true });
|
|
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import test from "node:test";
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
17
|
import { runGSDDoctor } from "../../doctor.ts";
|
|
18
|
-
import { closeDatabase } from "../../gsd-db.ts";
|
|
18
|
+
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../../gsd-db.ts";
|
|
19
19
|
|
|
20
20
|
function makeTmp(name: string): string {
|
|
21
21
|
const dir = join(tmpdir(), `doctor-fixlevel-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
@@ -177,6 +177,57 @@ test("legacy roadmap fallback: future slices are treated as pending, active slic
|
|
|
177
177
|
);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
test("db skipped slices do not report missing directories", async (t) => {
|
|
181
|
+
const tmp = makeTmp("skipped-slice-dir");
|
|
182
|
+
t.after(() => {
|
|
183
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
184
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const gsd = join(tmp, ".gsd");
|
|
188
|
+
const m = join(gsd, "milestones", "M001");
|
|
189
|
+
mkdirSync(m, { recursive: true });
|
|
190
|
+
|
|
191
|
+
writeFileSync(join(m, "M001-ROADMAP.md"), `# M001: Test
|
|
192
|
+
|
|
193
|
+
## Slices
|
|
194
|
+
|
|
195
|
+
- [ ] **S05: Skipped Slice** \`risk:low\` \`depends:[]\`
|
|
196
|
+
> Intentionally skipped
|
|
197
|
+
`);
|
|
198
|
+
|
|
199
|
+
openDatabase(join(gsd, "gsd.db"));
|
|
200
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
201
|
+
insertSlice({ id: "S05", milestoneId: "M001", title: "Skipped Slice", status: "skipped", sequence: 5 });
|
|
202
|
+
|
|
203
|
+
const report = await runGSDDoctor(tmp, { scope: "M001" });
|
|
204
|
+
const missingDirIssues = report.issues.filter(
|
|
205
|
+
i =>
|
|
206
|
+
(i.code === "missing_slice_dir" || i.code === "missing_tasks_dir") &&
|
|
207
|
+
i.unitId === "M001/S05",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.deepStrictEqual(
|
|
211
|
+
missingDirIssues,
|
|
212
|
+
[],
|
|
213
|
+
"skipped slices should not require slice or tasks directories",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("doctor source treats skipped DB slices as closed and directory-optional", () => {
|
|
218
|
+
const doctorSource = readFileSync(join(process.cwd(), "src/resources/extensions/gsd/doctor.ts"), "utf8");
|
|
219
|
+
assert.match(
|
|
220
|
+
doctorSource,
|
|
221
|
+
/done:\s*isClosedStatus\(s\.status\)/,
|
|
222
|
+
"doctor should normalize skipped DB slices through isClosedStatus()",
|
|
223
|
+
);
|
|
224
|
+
assert.match(
|
|
225
|
+
doctorSource,
|
|
226
|
+
/if \(slice\.pending \|\| slice\.skipped\) continue;/,
|
|
227
|
+
"doctor should skip missing-directory checks for skipped slices",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
180
231
|
test("fixLevel:all — delimiter_in_title still fixable", async (t) => {
|
|
181
232
|
const tmp = makeTmp("delimiter-fix");
|
|
182
233
|
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -661,10 +661,9 @@ describe('doctor-git', async () => {
|
|
|
661
661
|
env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
|
|
662
662
|
});
|
|
663
663
|
|
|
664
|
-
// Modify
|
|
665
|
-
//
|
|
664
|
+
// Modify an already-tracked file (nativeAddTracked uses git add -u,
|
|
665
|
+
// which only stages tracked files — new untracked files are not staged)
|
|
666
666
|
writeFileSync(join(dir, "README.md"), "# test\nmodified content\n");
|
|
667
|
-
writeFileSync(join(dir, "new-untracked.ts"), "export const preserved = true;\n");
|
|
668
667
|
|
|
669
668
|
const detect = await runGSDDoctor(dir);
|
|
670
669
|
const staleIssues = detect.issues.filter(i => i.code === "stale_uncommitted_changes");
|
|
@@ -682,12 +681,6 @@ describe('doctor-git', async () => {
|
|
|
682
681
|
// Verify the snapshot commit was created with the gsd snapshot tag
|
|
683
682
|
const log = run("git log -1 --oneline", dir);
|
|
684
683
|
assert.ok(log.includes("gsd snapshot"), "commit is tagged with gsd snapshot");
|
|
685
|
-
|
|
686
|
-
const files = run("git show --name-only --format= HEAD", dir);
|
|
687
|
-
assert.ok(files.includes("README.md"), "snapshot keeps tracked modifications");
|
|
688
|
-
assert.ok(files.includes("new-untracked.ts"), "snapshot also includes new untracked files");
|
|
689
|
-
const status = run("git status --short", dir);
|
|
690
|
-
assert.ok(!status.includes("new-untracked.ts"), "snapshot does not leave the new source file untracked");
|
|
691
684
|
});
|
|
692
685
|
|
|
693
686
|
// ─── Test: stale_uncommitted_changes NOT flagged when recent commit ──
|