gsd-pi 2.82.0-dev.725028083 → 2.82.0-dev.9d5798940
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 +3 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +3 -1
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
- package/dist/resources/extensions/cmux/index.js +5 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +113 -6
- package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +232 -126
- package/dist/resources/extensions/gsd/auto-prompts.js +2 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
- package/dist/resources/extensions/gsd/auto-start.js +7 -3
- package/dist/resources/extensions/gsd/auto-verification.js +28 -22
- package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
- package/dist/resources/extensions/gsd/auto.js +128 -52
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +16 -7
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -2
- package/dist/resources/extensions/gsd/clean-root-preflight.js +170 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +4 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +37 -0
- package/dist/resources/extensions/gsd/commands-bootstrap.js +5 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
- package/dist/resources/extensions/gsd/export-html.js +27 -425
- package/dist/resources/extensions/gsd/md-importer.js +1 -1
- package/dist/resources/extensions/gsd/migrate/command.js +5 -0
- package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
- package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
- package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
- package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
- package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +32 -10
- package/dist/resources/extensions/gsd/verification-verdict.js +26 -0
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +49 -9
- package/dist/resources/extensions/shared/html-shell.js +388 -0
- package/dist/resources/extensions/subagent/index.js +448 -78
- package/dist/resources/extensions/subagent/launch.js +77 -0
- package/dist/resources/extensions/subagent/run-store.js +148 -0
- package/dist/resources/extensions/visual-brief/artifact-policy.js +29 -0
- package/dist/resources/extensions/visual-brief/extension-manifest.json +8 -0
- package/dist/resources/extensions/visual-brief/index.js +5 -0
- package/dist/resources/extensions/visual-brief/page-contract.js +124 -0
- package/dist/resources/extensions/visual-brief/prompts.js +140 -0
- 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/react-loadable-manifest.json +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.js.nft.json +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 +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- 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 +4 -5
- 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 +2 -5
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -7
- 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 +4 -7
- 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 +4 -5
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page.js.nft.json +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 +15 -15
- package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
- 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/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/2973.33f26573894b6153.js +2 -0
- package/dist/web/standalone/.next/static/chunks/{8359.e059d86b255fce1c.js → 8359.7eb3bb8f8ecf4c01.js} +2 -2
- package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-de742b64187e13fe.js → webpack-9a4db269f9ed63ad.js} +1 -1
- package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
- package/package.json +3 -3
- package/packages/contracts/dist/rpc.test.js +7 -0
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/dist/workflow.d.ts +21 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +24 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/src/rpc.test.ts +8 -0
- package/packages/contracts/src/workflow.ts +24 -0
- package/packages/mcp-server/README.md +13 -4
- package/packages/mcp-server/dist/workflow-tools.d.ts +0 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +80 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
- package/packages/mcp-server/src/workflow-tools.ts +168 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +5 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +6 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/src/resources/GSD-WORKFLOW.md +3 -1
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
- package/src/resources/extensions/cmux/index.ts +6 -0
- package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
- package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +265 -138
- package/src/resources/extensions/gsd/auto-prompts.ts +2 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -3
- package/src/resources/extensions/gsd/auto-verification.ts +36 -34
- package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
- package/src/resources/extensions/gsd/auto.ts +136 -51
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +16 -6
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +19 -3
- package/src/resources/extensions/gsd/clean-root-preflight.ts +174 -8
- package/src/resources/extensions/gsd/commands/catalog.ts +4 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +40 -0
- package/src/resources/extensions/gsd/commands-bootstrap.ts +10 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
- package/src/resources/extensions/gsd/export-html.ts +27 -427
- package/src/resources/extensions/gsd/md-importer.ts +1 -1
- package/src/resources/extensions/gsd/migrate/command.ts +5 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
- package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
- package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/brief-command.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +107 -2
- package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +79 -1
- package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +86 -7
- package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +47 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +47 -11
- package/src/resources/extensions/gsd/verification-verdict.ts +47 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +54 -9
- package/src/resources/extensions/shared/html-shell.ts +412 -0
- package/src/resources/extensions/subagent/index.ts +567 -103
- package/src/resources/extensions/subagent/launch.ts +131 -0
- package/src/resources/extensions/subagent/run-store.ts +218 -0
- package/src/resources/extensions/subagent/tests/launch.test.ts +115 -0
- package/src/resources/extensions/subagent/tests/run-store.test.ts +111 -0
- package/src/resources/extensions/visual-brief/artifact-policy.ts +41 -0
- package/src/resources/extensions/visual-brief/extension-manifest.json +8 -0
- package/src/resources/extensions/visual-brief/index.ts +8 -0
- package/src/resources/extensions/visual-brief/page-contract.ts +136 -0
- package/src/resources/extensions/visual-brief/prompts.ts +183 -0
- package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +212 -0
- package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
- package/dist/web/standalone/.next/static/chunks/2556.0527fea66e123b7f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
- package/dist/web/standalone/.next/static/css/54ec2745c1da488b.css +0 -1
- package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
- package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// GSD-2 + /gsd brief command behavior tests
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
|
|
6
|
+
import { GSD_COMMAND_DESCRIPTION, getGsdArgumentCompletions } from "../commands/catalog.ts";
|
|
7
|
+
import { handleCoreCommand, showHelp } from "../commands/handlers/core.ts";
|
|
8
|
+
import { VISUAL_BRIEF_USAGE } from "../../visual-brief/prompts.ts";
|
|
9
|
+
|
|
10
|
+
function createMockCtx() {
|
|
11
|
+
const notifications: { message: string; level: string }[] = [];
|
|
12
|
+
return {
|
|
13
|
+
notifications,
|
|
14
|
+
ui: {
|
|
15
|
+
notify(message: string, level: string) {
|
|
16
|
+
notifications.push({ message, level });
|
|
17
|
+
},
|
|
18
|
+
custom: async () => undefined,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createMockPi() {
|
|
24
|
+
const sentMessages: string[] = [];
|
|
25
|
+
return {
|
|
26
|
+
sentMessages,
|
|
27
|
+
sendUserMessage(content: string) {
|
|
28
|
+
sentMessages.push(content);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("/gsd brief appears in the command description and top-level completions", () => {
|
|
34
|
+
assert.match(GSD_COMMAND_DESCRIPTION, /brief/);
|
|
35
|
+
|
|
36
|
+
const completions = getGsdArgumentCompletions("br");
|
|
37
|
+
const entry = completions.find((completion) => completion.value === "brief");
|
|
38
|
+
|
|
39
|
+
assert.ok(entry, "brief should appear in top-level completions");
|
|
40
|
+
assert.match(entry.description, /visual HTML brief/i);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("/gsd brief exposes Visual Brief mode completions", () => {
|
|
44
|
+
const completions = getGsdArgumentCompletions("brief d");
|
|
45
|
+
|
|
46
|
+
assert.ok(
|
|
47
|
+
completions.some((completion) => completion.value === "brief diagram"),
|
|
48
|
+
"diagram should be suggested as a /gsd brief mode",
|
|
49
|
+
);
|
|
50
|
+
assert.ok(
|
|
51
|
+
getGsdArgumentCompletions("brief ").some((completion) => completion.value === "brief diff"),
|
|
52
|
+
"diff should be suggested after /gsd brief",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("/gsd brief sends a Visual Brief prompt for valid args", async () => {
|
|
57
|
+
const ctx = createMockCtx();
|
|
58
|
+
const pi = createMockPi();
|
|
59
|
+
|
|
60
|
+
const handled = await handleCoreCommand("brief diagram extension loading lifecycle", ctx as any, pi as any);
|
|
61
|
+
|
|
62
|
+
assert.equal(handled, true);
|
|
63
|
+
assert.equal(ctx.notifications.length, 0);
|
|
64
|
+
assert.equal(pi.sentMessages.length, 1);
|
|
65
|
+
assert.match(pi.sentMessages[0], /Mode: diagram/);
|
|
66
|
+
assert.match(pi.sentMessages[0], /Subject: extension loading lifecycle/);
|
|
67
|
+
assert.match(pi.sentMessages[0], /Output directory:/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("/gsd brief reports usage for empty args instead of sending a prompt", async () => {
|
|
71
|
+
const ctx = createMockCtx();
|
|
72
|
+
const pi = createMockPi();
|
|
73
|
+
|
|
74
|
+
const handled = await handleCoreCommand("brief", ctx as any, pi as any);
|
|
75
|
+
|
|
76
|
+
assert.equal(handled, true);
|
|
77
|
+
assert.equal(pi.sentMessages.length, 0);
|
|
78
|
+
assert.deepEqual(ctx.notifications, [{ message: VISUAL_BRIEF_USAGE, level: "info" }]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("/gsd help full lists brief separately from visualize", () => {
|
|
82
|
+
const ctx = createMockCtx();
|
|
83
|
+
|
|
84
|
+
showHelp(ctx as any, "full");
|
|
85
|
+
|
|
86
|
+
const help = ctx.notifications.at(0)?.message ?? "";
|
|
87
|
+
assert.match(help, /\/gsd visualize\s+Interactive 10-tab TUI/);
|
|
88
|
+
assert.match(help, /\/gsd brief <mode>\s+Generate a visual HTML brief/);
|
|
89
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, realpathSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, realpathSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
@@ -208,8 +208,8 @@ test("postflightPopStash conflict warning names the exact stash ref", () => {
|
|
|
208
208
|
assert.match(postflight.message, /failed after merge of milestone M005C/);
|
|
209
209
|
|
|
210
210
|
const warning = notifications.find((n) => n.level === "warning")?.msg ?? "";
|
|
211
|
-
assert.match(warning, /git stash pop stash@\{\d+\}/);
|
|
212
211
|
assert.match(warning, /git stash apply stash@\{\d+\}/);
|
|
212
|
+
assert.match(warning, /git stash drop stash@\{\d+\}/);
|
|
213
213
|
} finally {
|
|
214
214
|
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
215
215
|
}
|
|
@@ -279,3 +279,108 @@ test("postflightPopStash falls back to milestone marker prefix when exact marker
|
|
|
279
279
|
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
280
280
|
}
|
|
281
281
|
});
|
|
282
|
+
|
|
283
|
+
test("postflightPopStash preserves stash when untracked collision differs from merged file", () => {
|
|
284
|
+
const repo = createTempRepo();
|
|
285
|
+
try {
|
|
286
|
+
writeFileSync(join(repo, "tests.txt"), "local preflight test\n");
|
|
287
|
+
const preflight = preflightCleanRoot(repo, "M009", () => {});
|
|
288
|
+
assert.equal(preflight.stashPushed, true, "preflight must stash untracked file");
|
|
289
|
+
|
|
290
|
+
writeFileSync(join(repo, "tests.txt"), "merged milestone test\n");
|
|
291
|
+
run("git add tests.txt", repo);
|
|
292
|
+
run('git commit -m "feat: add merged test"', repo);
|
|
293
|
+
|
|
294
|
+
const notifications: Array<{ msg: string; level: string }> = [];
|
|
295
|
+
const postflight = postflightPopStash(repo, "M009", preflight.stashMarker, (msg, level) => {
|
|
296
|
+
notifications.push({ msg, level });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
assert.equal(postflight.needsManualRecovery, false, "different already-present untracked files must not stop auto-mode");
|
|
300
|
+
assert.equal(postflight.resolution, "already-present-preserved");
|
|
301
|
+
assert.deepEqual(postflight.collidedPaths, ["tests.txt"]);
|
|
302
|
+
assert.equal(readFileSync(join(repo, "tests.txt"), "utf-8"), "merged milestone test\n");
|
|
303
|
+
assert.equal(run("git status --porcelain", repo), "", "merged file must stay clean");
|
|
304
|
+
|
|
305
|
+
const stashList = run("git stash list", repo);
|
|
306
|
+
assert.ok(preflight.stashMarker && stashList.includes(preflight.stashMarker), "stash backup must be preserved");
|
|
307
|
+
assert.ok(
|
|
308
|
+
notifications.some((n) => n.level === "warning" && n.msg.includes("preserving")),
|
|
309
|
+
"user must be warned that the stash was preserved as backup",
|
|
310
|
+
);
|
|
311
|
+
} finally {
|
|
312
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("postflightPopStash drops stash when untracked collision is identical to merged file", () => {
|
|
317
|
+
const repo = createTempRepo();
|
|
318
|
+
try {
|
|
319
|
+
writeFileSync(join(repo, "tests.txt"), "same test content\n");
|
|
320
|
+
const preflight = preflightCleanRoot(repo, "M010", () => {});
|
|
321
|
+
assert.equal(preflight.stashPushed, true, "preflight must stash untracked file");
|
|
322
|
+
|
|
323
|
+
writeFileSync(join(repo, "tests.txt"), "same test content\n");
|
|
324
|
+
run("git add tests.txt", repo);
|
|
325
|
+
run('git commit -m "feat: add same test"', repo);
|
|
326
|
+
|
|
327
|
+
const postflight = postflightPopStash(repo, "M010", preflight.stashMarker, () => {});
|
|
328
|
+
|
|
329
|
+
assert.equal(postflight.needsManualRecovery, false, "identical already-present files must not stop auto-mode");
|
|
330
|
+
assert.equal(postflight.resolution, "already-present-dropped");
|
|
331
|
+
assert.equal(readFileSync(join(repo, "tests.txt"), "utf-8"), "same test content\n");
|
|
332
|
+
|
|
333
|
+
const stashList = run("git stash list", repo);
|
|
334
|
+
assert.ok(!stashList.includes(preflight.stashMarker ?? ""), "identical stash must be dropped");
|
|
335
|
+
} finally {
|
|
336
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("postflightPopStash requires manual recovery for mixed tracked changes and untracked collision", () => {
|
|
341
|
+
const repo = createTempRepo();
|
|
342
|
+
try {
|
|
343
|
+
writeFileSync(join(repo, "README.md"), "# local tracked work\n");
|
|
344
|
+
writeFileSync(join(repo, "tests.txt"), "local untracked work\n");
|
|
345
|
+
const preflight = preflightCleanRoot(repo, "M011", () => {});
|
|
346
|
+
assert.equal(preflight.stashPushed, true, "preflight must stash mixed changes");
|
|
347
|
+
|
|
348
|
+
writeFileSync(join(repo, "tests.txt"), "merged milestone test\n");
|
|
349
|
+
run("git add tests.txt", repo);
|
|
350
|
+
run('git commit -m "feat: add merged test"', repo);
|
|
351
|
+
|
|
352
|
+
const postflight = postflightPopStash(repo, "M011", preflight.stashMarker, () => {});
|
|
353
|
+
|
|
354
|
+
assert.equal(postflight.needsManualRecovery, true, "tracked stash payload must still require manual recovery");
|
|
355
|
+
assert.equal(postflight.resolution, "manual-recovery");
|
|
356
|
+
const stashList = run("git stash list", repo);
|
|
357
|
+
assert.ok(preflight.stashMarker && stashList.includes(preflight.stashMarker), "mixed stash must be preserved");
|
|
358
|
+
} finally {
|
|
359
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("postflightPopStash requires manual recovery when an untracked stash path is missing after collision", () => {
|
|
364
|
+
const repo = createTempRepo();
|
|
365
|
+
try {
|
|
366
|
+
writeFileSync(join(repo, "tests.txt"), "local untracked work\n");
|
|
367
|
+
writeFileSync(join(repo, "other-tests.txt"), "other local untracked work\n");
|
|
368
|
+
const preflight = preflightCleanRoot(repo, "M012", () => {});
|
|
369
|
+
assert.equal(preflight.stashPushed, true, "preflight must stash untracked files");
|
|
370
|
+
|
|
371
|
+
writeFileSync(join(repo, "tests.txt"), "merged milestone test\n");
|
|
372
|
+
run("git add tests.txt", repo);
|
|
373
|
+
run('git commit -m "feat: add one merged test"', repo);
|
|
374
|
+
|
|
375
|
+
const postflight = postflightPopStash(repo, "M012", preflight.stashMarker, () => {});
|
|
376
|
+
|
|
377
|
+
assert.equal(postflight.needsManualRecovery, true, "partial untracked restores must still require manual recovery");
|
|
378
|
+
assert.equal(postflight.resolution, "manual-recovery");
|
|
379
|
+
assert.equal(existsSync(join(repo, "other-tests.txt")), true, "git may partially restore the non-colliding path");
|
|
380
|
+
assert.match(run("git status --porcelain", repo), /\?\? other-tests\.txt/, "partial restore must leave manual recovery visible");
|
|
381
|
+
const stashList = run("git stash list", repo);
|
|
382
|
+
assert.ok(preflight.stashMarker && stashList.includes(preflight.stashMarker), "stash must remain for manual recovery");
|
|
383
|
+
} finally {
|
|
384
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
385
|
+
}
|
|
386
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Tests closeout git action deferral policy for auto-mode units.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
|
|
7
|
+
import { shouldDeferCloseoutGitAction } from "../auto-post-unit.ts";
|
|
8
|
+
|
|
9
|
+
test("execute-task defers closeout git action until verification passes", () => {
|
|
10
|
+
assert.equal(shouldDeferCloseoutGitAction("execute-task"), true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("non execute-task units keep pre-verification closeout git action", () => {
|
|
14
|
+
assert.equal(shouldDeferCloseoutGitAction("plan-slice"), false);
|
|
15
|
+
assert.equal(shouldDeferCloseoutGitAction("complete-slice"), false);
|
|
16
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, 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";
|
|
@@ -10,7 +10,7 @@ import { runDispatch, runPreDispatch } from "../auto/phases.ts";
|
|
|
10
10
|
import { AutoSession } from "../auto/session.ts";
|
|
11
11
|
import { resolveUnitSupervisionTimeouts } from "../auto-timers.ts";
|
|
12
12
|
import { bootstrapAutoSession } from "../auto-start.ts";
|
|
13
|
-
import { postUnitPreVerification } from "../auto-post-unit.ts";
|
|
13
|
+
import { postUnitPostVerification, postUnitPreVerification } from "../auto-post-unit.ts";
|
|
14
14
|
import { resolveDispatch, setResearchProjectPromptBuilderForTest } from "../auto-dispatch.ts";
|
|
15
15
|
import { resolveExpectedArtifactPath, verifyExpectedArtifact, writeBlockerPlaceholder } from "../auto-recovery.ts";
|
|
16
16
|
import { finalizeProjectResearchTimeout } from "../project-research-policy.ts";
|
|
@@ -1587,6 +1587,61 @@ test("deep project setup: discuss-milestone question failure pauses instead of a
|
|
|
1587
1587
|
}
|
|
1588
1588
|
});
|
|
1589
1589
|
|
|
1590
|
+
test("verified task git closeout failure retries and continues auto-mode", async () => {
|
|
1591
|
+
const base = makeBase();
|
|
1592
|
+
try {
|
|
1593
|
+
execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
|
|
1594
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base, stdio: "ignore" });
|
|
1595
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: base, stdio: "ignore" });
|
|
1596
|
+
const hookPath = join(base, ".git", "hooks", "pre-commit");
|
|
1597
|
+
writeFileSync(
|
|
1598
|
+
hookPath,
|
|
1599
|
+
[
|
|
1600
|
+
"#!/bin/sh",
|
|
1601
|
+
"count_file=.git/pre-commit-count",
|
|
1602
|
+
"count=0",
|
|
1603
|
+
"if [ -f \"$count_file\" ]; then count=$(cat \"$count_file\"); fi",
|
|
1604
|
+
"count=$((count + 1))",
|
|
1605
|
+
"printf \"%s\" \"$count\" > \"$count_file\"",
|
|
1606
|
+
"echo blocked by test hook >&2",
|
|
1607
|
+
"exit 1",
|
|
1608
|
+
].join("\n"),
|
|
1609
|
+
);
|
|
1610
|
+
chmodSync(hookPath, 0o755);
|
|
1611
|
+
writeFileSync(join(base, "work.txt"), "changed\n");
|
|
1612
|
+
|
|
1613
|
+
const s = new AutoSession();
|
|
1614
|
+
s.active = true;
|
|
1615
|
+
s.basePath = base;
|
|
1616
|
+
s.originalBasePath = base;
|
|
1617
|
+
s.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
|
|
1618
|
+
|
|
1619
|
+
let pauseCalled = false;
|
|
1620
|
+
const notifications: Array<{ message: string; severity?: string }> = [];
|
|
1621
|
+
const result = await postUnitPostVerification({
|
|
1622
|
+
s,
|
|
1623
|
+
ctx: { ui: { notify: (message: string, severity?: string) => notifications.push({ message, severity }) } } as any,
|
|
1624
|
+
pi: {} as any,
|
|
1625
|
+
buildSnapshotOpts: () => ({}) as any,
|
|
1626
|
+
lockBase: () => base,
|
|
1627
|
+
stopAuto: async () => {},
|
|
1628
|
+
pauseAuto: async () => { pauseCalled = true; },
|
|
1629
|
+
updateProgressWidget: () => {},
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
assert.equal(result, "continue");
|
|
1633
|
+
assert.equal(pauseCalled, false);
|
|
1634
|
+
assert.equal(s.lastGitActionStatus, "failed");
|
|
1635
|
+
assert.equal(readFileSync(join(base, ".git", "pre-commit-count"), "utf-8"), "3");
|
|
1636
|
+
assert.ok(
|
|
1637
|
+
notifications.some((entry) => entry.severity === "warning" && entry.message.includes("Git commit failed")),
|
|
1638
|
+
"verified task git closeout failure should warn instead of stopping auto-mode",
|
|
1639
|
+
);
|
|
1640
|
+
} finally {
|
|
1641
|
+
rmSync(base, { recursive: true, force: true });
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1590
1645
|
test("deep project setup: approval wait wins over deterministic write-gate placeholder", async () => {
|
|
1591
1646
|
const base = makeBase();
|
|
1592
1647
|
try {
|
|
@@ -118,3 +118,42 @@ describe("complete phase dispatch guard (#5683)", () => {
|
|
|
118
118
|
assert.equal(result?.reason, "All milestones complete.");
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
describe("complete milestone context recovery guard (#5831)", () => {
|
|
123
|
+
let base = "";
|
|
124
|
+
const executionEntryRule = DISPATCH_RULES.find(
|
|
125
|
+
(candidate) => candidate.name === "execution-entry phase (no context) → discuss-milestone",
|
|
126
|
+
);
|
|
127
|
+
const prePlanningRule = DISPATCH_RULES.find(
|
|
128
|
+
(candidate) => candidate.name === "pre-planning (no context) → discuss-milestone",
|
|
129
|
+
);
|
|
130
|
+
assert.ok(executionEntryRule, "execution-entry missing-context rule should exist");
|
|
131
|
+
assert.ok(prePlanningRule, "pre-planning missing-context rule should exist");
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
if (base) rmSync(base, { recursive: true, force: true });
|
|
135
|
+
base = "";
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("does not discuss a complete execution-entry milestone with no CONTEXT file", async () => {
|
|
139
|
+
base = makeBase();
|
|
140
|
+
const ctx = buildDispatchCtx(base);
|
|
141
|
+
ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
|
|
142
|
+
ctx.state.phase = "completing-milestone";
|
|
143
|
+
|
|
144
|
+
const result = await executionEntryRule.match(ctx);
|
|
145
|
+
|
|
146
|
+
assert.equal(result, null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("does not discuss a complete pre-planning milestone with no CONTEXT file", async () => {
|
|
150
|
+
base = makeBase();
|
|
151
|
+
const ctx = buildDispatchCtx(base);
|
|
152
|
+
ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
|
|
153
|
+
ctx.state.phase = "pre-planning";
|
|
154
|
+
|
|
155
|
+
const result = await prePlanningRule.match(ctx);
|
|
156
|
+
|
|
157
|
+
assert.equal(result, null);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Tests for verification evidence cross-reference mismatch policy.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
|
|
7
|
+
import { crossReferenceEvidence } from "../safety/evidence-cross-ref.ts";
|
|
8
|
+
import type { EvidenceEntry } from "../safety/evidence-collector.ts";
|
|
9
|
+
|
|
10
|
+
test("claims of passing verification become errors when recorded bash evidence failed", () => {
|
|
11
|
+
const mismatches = crossReferenceEvidence(
|
|
12
|
+
[{ command: "npm test", exitCode: 0, verdict: "passed" }],
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
kind: "bash",
|
|
16
|
+
toolCallId: "call-1",
|
|
17
|
+
command: "npm test",
|
|
18
|
+
exitCode: 1,
|
|
19
|
+
outputSnippet: "failed",
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
},
|
|
22
|
+
] as EvidenceEntry[],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
assert.equal(mismatches.length, 1);
|
|
26
|
+
assert.equal(mismatches[0].severity, "error");
|
|
27
|
+
assert.match(mismatches[0].reason, /Claimed exitCode=0/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("missing recorded bash evidence remains a warning", () => {
|
|
31
|
+
const mismatches = crossReferenceEvidence(
|
|
32
|
+
[{ command: "npm test", exitCode: 0, verdict: "passed" }],
|
|
33
|
+
[],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
assert.equal(mismatches.length, 1);
|
|
37
|
+
assert.equal(mismatches[0].severity, "warning");
|
|
38
|
+
});
|
|
@@ -141,6 +141,14 @@ test("Feature 1: executive summary paragraph is rendered", () => {
|
|
|
141
141
|
assert.ok(html.includes("$2.50 spent"), "should contain cost");
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test("report uses the shared GSD HTML shell", () => {
|
|
145
|
+
const html = generateHtmlReport(mockData(), mockOpts());
|
|
146
|
+
assert.ok(html.includes('<span class="logo">GSD</span>'), "should render shared shell logo");
|
|
147
|
+
assert.ok(html.includes('<span class="kind-chip">Report</span>'), "should render report kind chip");
|
|
148
|
+
assert.ok(html.includes('<nav class="toc" aria-label="Report sections">'), "should render shared shell TOC");
|
|
149
|
+
assert.ok(html.includes('<main>'), "should render content inside shared shell main");
|
|
150
|
+
});
|
|
151
|
+
|
|
144
152
|
test("Feature 1: executive summary includes budget context when set", () => {
|
|
145
153
|
const data = mockData({ health: { ...mockData().health, budgetCeiling: 10.00 } });
|
|
146
154
|
const html = generateHtmlReport(data, mockOpts());
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
generatePreview,
|
|
15
15
|
writeGSDDirectory,
|
|
16
16
|
} from '../../migrate/index.ts';
|
|
17
|
+
import { importWrittenMigrationToDb } from '../../migrate/command.ts';
|
|
17
18
|
import { deriveState } from '../../state.ts';
|
|
19
|
+
import { closeDatabase, getDecisionById, getRequirementCounts } from '../../gsd-db.ts';
|
|
18
20
|
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
19
21
|
import assert from 'node:assert/strict';
|
|
20
22
|
|
|
@@ -52,6 +54,16 @@ const SAMPLE_REQUIREMENTS = `# Requirements
|
|
|
52
54
|
- Description: Output matches GSD format.
|
|
53
55
|
`;
|
|
54
56
|
|
|
57
|
+
const SAMPLE_REQUIREMENTS_LEGACY_IDS = `# Requirements
|
|
58
|
+
|
|
59
|
+
## Active
|
|
60
|
+
|
|
61
|
+
- [ ] **CORE-PIPELINE**: Pipeline must work end-to-end.
|
|
62
|
+
- [ ] **OUTPUT-FORMAT**: Output matches GSD format.
|
|
63
|
+
- [ ] **IMPORT-DB**: Migration imports requirements into the DB.
|
|
64
|
+
- [ ] **STATUS-WIDGET**: Status can query migrated requirements.
|
|
65
|
+
`;
|
|
66
|
+
|
|
55
67
|
const SAMPLE_STATE = `# State
|
|
56
68
|
|
|
57
69
|
**Current Phase:** 20-features
|
|
@@ -166,14 +178,14 @@ Depends on foundation work.
|
|
|
166
178
|
</context>
|
|
167
179
|
`;
|
|
168
180
|
|
|
169
|
-
function createCompleteFixture(): string {
|
|
181
|
+
function createCompleteFixture(requirementsContent: string = SAMPLE_REQUIREMENTS): string {
|
|
170
182
|
const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-test-'));
|
|
171
183
|
const planning = join(base, '.planning');
|
|
172
184
|
mkdirSync(planning, { recursive: true });
|
|
173
185
|
|
|
174
186
|
writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT);
|
|
175
187
|
writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP);
|
|
176
|
-
writeFileSync(join(planning, 'REQUIREMENTS.md'),
|
|
188
|
+
writeFileSync(join(planning, 'REQUIREMENTS.md'), requirementsContent);
|
|
177
189
|
writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE);
|
|
178
190
|
writeFileSync(join(planning, 'config.json'), SAMPLE_CONFIG);
|
|
179
191
|
|
|
@@ -303,6 +315,7 @@ test('Full pipeline: parse → transform → preview → write → deriveState',
|
|
|
303
315
|
const expectedTaskPct = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;
|
|
304
316
|
assert.deepStrictEqual(preview.sliceCompletionPct, expectedSlicePct, 'pipeline: preview sliceCompletionPct');
|
|
305
317
|
assert.deepStrictEqual(preview.taskCompletionPct, expectedTaskPct, 'pipeline: preview taskCompletionPct');
|
|
318
|
+
assert.deepStrictEqual(preview.decisions.total, 1, 'pipeline: preview decisions total');
|
|
306
319
|
|
|
307
320
|
// Requirements in preview
|
|
308
321
|
assert.deepStrictEqual(preview.requirements.active, 1, 'pipeline: preview requirements active');
|
|
@@ -342,6 +355,39 @@ test('Full pipeline: parse → transform → preview → write → deriveState',
|
|
|
342
355
|
}
|
|
343
356
|
});
|
|
344
357
|
|
|
358
|
+
test('Full pipeline: legacy requirement IDs import into DB with canonical IDs', async () => {
|
|
359
|
+
const base = createCompleteFixture(SAMPLE_REQUIREMENTS_LEGACY_IDS);
|
|
360
|
+
const writeTarget = mkdtempSync(join(tmpdir(), 'gsd-cmd-legacy-reqs-'));
|
|
361
|
+
try {
|
|
362
|
+
const parsed = await parsePlanningDirectory(join(base, '.planning'));
|
|
363
|
+
const project = transformToGSD(parsed);
|
|
364
|
+
const preview = generatePreview(project);
|
|
365
|
+
|
|
366
|
+
assert.deepStrictEqual(
|
|
367
|
+
project.requirements.map((req) => req.id),
|
|
368
|
+
['R001', 'R002', 'R003', 'R004'],
|
|
369
|
+
'legacy-reqs: transform assigns canonical R IDs',
|
|
370
|
+
);
|
|
371
|
+
assert.ok(
|
|
372
|
+
project.requirements[0]?.description.includes('Legacy ID: CORE-PIPELINE'),
|
|
373
|
+
'legacy-reqs: original ID survives in migrated requirement content',
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await writeGSDDirectory(project, writeTarget);
|
|
377
|
+
const imported = await importWrittenMigrationToDb(writeTarget, preview);
|
|
378
|
+
const counts = getRequirementCounts();
|
|
379
|
+
|
|
380
|
+
assert.deepStrictEqual(imported.decisions, 1, 'legacy-reqs: DB import includes migrated decisions');
|
|
381
|
+
assert.deepStrictEqual(imported.requirements, 4, 'legacy-reqs: DB import count matches preview');
|
|
382
|
+
assert.ok(getDecisionById('D001') !== null, 'legacy-reqs: migrated decision is queryable');
|
|
383
|
+
assert.deepStrictEqual(counts.total, 4, 'legacy-reqs: DB stores all migrated requirements');
|
|
384
|
+
} finally {
|
|
385
|
+
closeDatabase();
|
|
386
|
+
rmSync(base, { recursive: true, force: true });
|
|
387
|
+
rmSync(writeTarget, { recursive: true, force: true });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
345
391
|
// ─── Test 6: .gsd/ exists detection ────────────────────────────────────
|
|
346
392
|
|
|
347
393
|
test('.gsd/ exists detection', () => {
|
|
@@ -357,4 +403,3 @@ test('.gsd/ exists detection', () => {
|
|
|
357
403
|
rmSync(base, { recursive: true, force: true });
|
|
358
404
|
}
|
|
359
405
|
});
|
|
360
|
-
|
|
@@ -497,6 +497,7 @@ test('Scenario 11: Requirements edge cases', () => {
|
|
|
497
497
|
makeRequirement('', 'Another No ID', 'validated'),
|
|
498
498
|
makeRequirement('R005', 'Has ID', 'something-weird'),
|
|
499
499
|
makeRequirement('R006', 'Deferred One', 'DEFERRED'),
|
|
500
|
+
makeRequirement('AUTH-7', 'Legacy ID', 'active'),
|
|
500
501
|
],
|
|
501
502
|
phases: {
|
|
502
503
|
'1-req-edge': makePhase('1-req-edge', 1, 'req-edge'),
|
|
@@ -510,6 +511,8 @@ test('Scenario 11: Requirements edge cases', () => {
|
|
|
510
511
|
assert.deepStrictEqual(result.requirements[2]?.id, 'R005', 'req-edge: existing id preserved');
|
|
511
512
|
assert.deepStrictEqual(result.requirements[2]?.status, 'active', 'req-edge: unknown status normalized to active');
|
|
512
513
|
assert.deepStrictEqual(result.requirements[3]?.status, 'deferred', 'req-edge: uppercase DEFERRED normalized');
|
|
514
|
+
assert.deepStrictEqual(result.requirements[4]?.id, 'R003', 'req-edge: non-R legacy id gets next canonical id');
|
|
515
|
+
assert.ok(result.requirements[4]?.description.includes('Legacy ID: AUTH-7'), 'req-edge: original legacy id is preserved in description');
|
|
513
516
|
});
|
|
514
517
|
|
|
515
518
|
// ─── Scenario 12: Vision derivation ────────────────────────────────────────
|
|
@@ -553,6 +556,8 @@ test('Scenario 13: Decisions content', () => {
|
|
|
553
556
|
const result = transformToGSD(project);
|
|
554
557
|
|
|
555
558
|
assert.ok(result.decisionsContent.includes('decision-01'), 'decisions: extracts key-decisions from summaries');
|
|
559
|
+
assert.ok(result.decisionsContent.includes('| D001 |'), 'decisions: writes DB-importable decision ID');
|
|
560
|
+
assert.ok(result.decisionsContent.includes('| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |'), 'decisions: writes canonical table header');
|
|
556
561
|
});
|
|
557
562
|
|
|
558
563
|
// ─── Scenario 14: No undefined values in output ───────────────────────────
|
|
@@ -616,4 +621,3 @@ test('Scenario 15: Empty research', () => {
|
|
|
616
621
|
});
|
|
617
622
|
|
|
618
623
|
// ─── Results ───────────────────────────────────────────────────────────────
|
|
619
|
-
|
|
@@ -14,7 +14,7 @@ import { parseSummary } from '../files.ts';
|
|
|
14
14
|
import { deriveState } from '../state.ts';
|
|
15
15
|
import { invalidateAllCaches } from '../cache.ts';
|
|
16
16
|
import { ensureDbOpen } from '../bootstrap/dynamic-tools.ts';
|
|
17
|
-
import { closeDatabase, getAllMilestones } from '../gsd-db.ts';
|
|
17
|
+
import { closeDatabase, getAllMilestones, getArtifact } from '../gsd-db.ts';
|
|
18
18
|
import { importWrittenMigrationToDb } from '../migrate/command.ts';
|
|
19
19
|
import type {
|
|
20
20
|
GSDProject,
|
|
@@ -336,7 +336,12 @@ test('Scenario 2: Fully complete project — deriveState phase', async () => {
|
|
|
336
336
|
assert.deepStrictEqual(preview.taskCompletionPct, 100, 'complete: preview taskCompletionPct');
|
|
337
337
|
assert.deepStrictEqual(preview.requirements.total, 0, 'complete: preview requirements total');
|
|
338
338
|
|
|
339
|
+
const imported = await importWrittenMigrationToDb(base, preview);
|
|
340
|
+
assert.ok(imported.artifacts >= 6, 'complete: imports generated milestone artifacts');
|
|
341
|
+
assert.ok(getArtifact('milestones/M001/M001-VALIDATION.md') !== null, 'complete: M001-VALIDATION.md imported as artifact');
|
|
342
|
+
assert.ok(getArtifact('milestones/M001/M001-SUMMARY.md') !== null, 'complete: M001-SUMMARY.md imported as artifact');
|
|
339
343
|
} finally {
|
|
344
|
+
closeDatabase();
|
|
340
345
|
rmSync(base, { recursive: true, force: true });
|
|
341
346
|
}
|
|
342
347
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { test } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
|
|
@@ -48,6 +48,60 @@ test("parkMilestone updates DB status to 'parked' (#2694)", () => {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test("parkMilestone ignores blocked SUMMARY.md when DB milestone is active (#5828)", () => {
|
|
52
|
+
const base = createBase();
|
|
53
|
+
try {
|
|
54
|
+
openDatabase(":memory:");
|
|
55
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
|
|
58
|
+
[
|
|
59
|
+
"---",
|
|
60
|
+
"status: closeout_blocked",
|
|
61
|
+
"---",
|
|
62
|
+
"",
|
|
63
|
+
"# M001 Summary",
|
|
64
|
+
"",
|
|
65
|
+
"Completion was not persisted.",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
"utf-8",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const parked = parkMilestone(base, "M001", "test");
|
|
71
|
+
|
|
72
|
+
assert.ok(parked, "active DB row should allow parking despite a blocked SUMMARY.md");
|
|
73
|
+
assert.ok(
|
|
74
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
|
|
75
|
+
"PARKED.md should be written",
|
|
76
|
+
);
|
|
77
|
+
assert.equal(getMilestone("M001")!.status, "parked", "DB status should be parked");
|
|
78
|
+
} finally {
|
|
79
|
+
closeDatabase();
|
|
80
|
+
rmSync(base, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parkMilestone refuses DB-complete milestones (#5828)", () => {
|
|
85
|
+
const base = createBase();
|
|
86
|
+
try {
|
|
87
|
+
openDatabase(":memory:");
|
|
88
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
89
|
+
|
|
90
|
+
const parked = parkMilestone(base, "M001", "test");
|
|
91
|
+
|
|
92
|
+
assert.equal(parked, false, "complete DB row should not be parkable");
|
|
93
|
+
assert.equal(
|
|
94
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
|
|
95
|
+
false,
|
|
96
|
+
"PARKED.md should not be written",
|
|
97
|
+
);
|
|
98
|
+
assert.equal(getMilestone("M001")!.status, "complete", "DB status should remain complete");
|
|
99
|
+
} finally {
|
|
100
|
+
closeDatabase();
|
|
101
|
+
rmSync(base, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
51
105
|
test("unparkMilestone updates DB status to 'active' (#2694)", () => {
|
|
52
106
|
const base = createBase();
|
|
53
107
|
try {
|
|
@@ -125,6 +125,31 @@ test('handlePlanSlice advances DB-derived state out of planning immediately', as
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test('handlePlanSlice clears sketch flag so DB-derived state leaves refining', async () => {
|
|
129
|
+
const base = makeTmpBase();
|
|
130
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
|
|
134
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.', isSketch: true });
|
|
135
|
+
|
|
136
|
+
invalidateStateCache();
|
|
137
|
+
const before = await deriveState(base);
|
|
138
|
+
assert.equal(before.phase, 'refining');
|
|
139
|
+
|
|
140
|
+
const result = await handlePlanSlice(validParams(), base);
|
|
141
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
142
|
+
assert.equal(getSlice('M001', 'S02')?.is_sketch, 0, 'planned slice must no longer be treated as a sketch');
|
|
143
|
+
|
|
144
|
+
invalidateStateCache();
|
|
145
|
+
const after = await deriveState(base);
|
|
146
|
+
assert.notEqual(after.phase, 'refining');
|
|
147
|
+
assert.equal(after.progress?.tasks?.total, 2);
|
|
148
|
+
} finally {
|
|
149
|
+
cleanup(base);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
128
153
|
test('handlePlanSlice leaves omitted enrichment fields empty instead of rendering placeholders', async () => {
|
|
129
154
|
const base = makeTmpBase();
|
|
130
155
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|