gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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 +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- 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 +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -12,7 +12,6 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
|
-
import { extractSourceRegion } from "./test-helpers.ts";
|
|
16
15
|
|
|
17
16
|
test("#2684: preferences files are NOT in ROOT_STATE_FILES (forward-only sync)", () => {
|
|
18
17
|
const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
@@ -41,22 +40,9 @@ test("#2684: preferences files are NOT in ROOT_STATE_FILES (forward-only sync)",
|
|
|
41
40
|
);
|
|
42
41
|
});
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Find the copyPlanningArtifacts function body
|
|
49
|
-
const fnIdx = src.indexOf("function copyPlanningArtifacts");
|
|
50
|
-
assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
|
|
51
|
-
|
|
52
|
-
// Extract function body (up to the next top-level function)
|
|
53
|
-
const fnBody = extractSourceRegion(src, "function copyPlanningArtifacts");
|
|
54
|
-
|
|
55
|
-
assert.ok(
|
|
56
|
-
fnBody.includes("PROJECT_PREFERENCES_FILE") && fnBody.includes("LEGACY_PROJECT_PREFERENCES_FILE"),
|
|
57
|
-
"copyPlanningArtifacts should prefer canonical PREFERENCES.md and retain lowercase fallback via the shared constants",
|
|
58
|
-
);
|
|
59
|
-
});
|
|
43
|
+
// Phase C: copyPlanningArtifacts was deleted. Worktrees no longer
|
|
44
|
+
// maintain a parallel .gsd/ projection; preference seeding is now
|
|
45
|
+
// handled exclusively by syncGsdStateToWorktree() (covered below).
|
|
60
46
|
|
|
61
47
|
test("syncGsdStateToWorktree copies canonical PREFERENCES.md", async () => {
|
|
62
48
|
// Functional test: create a mock source and destination, call the sync
|
|
@@ -4,11 +4,16 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
7
|
+
const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
|
|
7
8
|
|
|
8
9
|
function readPrompt(name: string): string {
|
|
9
10
|
return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function readTemplate(name: string): string {
|
|
14
|
+
return readFileSync(join(templatesDir, `${name}.md`), "utf-8");
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
|
|
13
18
|
const prompt = readPrompt("reactive-execute");
|
|
14
19
|
assert.match(prompt, /subagent-written summary as authoritative/i);
|
|
@@ -174,15 +179,15 @@ test("guided-resume-task prompt preserves recovery state until work is supersede
|
|
|
174
179
|
|
|
175
180
|
// ─── Prompt migration: execute-task → gsd_complete_task ───────────────
|
|
176
181
|
|
|
177
|
-
test("execute-task prompt references
|
|
182
|
+
test("execute-task prompt references gsd_task_complete tool", () => {
|
|
178
183
|
const prompt = readPrompt("execute-task");
|
|
179
|
-
assert.match(prompt, /
|
|
184
|
+
assert.match(prompt, /gsd_task_complete/);
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
test("execute-task prompt uses
|
|
187
|
+
test("execute-task prompt uses gsd_task_complete as canonical summary write path", () => {
|
|
183
188
|
const prompt = readPrompt("execute-task");
|
|
184
189
|
assert.match(prompt, /\{\{taskSummaryPath\}\}/);
|
|
185
|
-
assert.match(prompt, /
|
|
190
|
+
assert.match(prompt, /gsd_task_complete/);
|
|
186
191
|
assert.match(prompt, /DB-backed tool is the canonical write path/i);
|
|
187
192
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{taskSummaryPath\}\}`?/i);
|
|
188
193
|
assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{taskSummaryPath\}\}`?\s*$/m);
|
|
@@ -202,9 +207,9 @@ test("execute-task prompt still contains template variables for context", () =>
|
|
|
202
207
|
|
|
203
208
|
// ─── Prompt migration: complete-slice → gsd_complete_slice ────────────
|
|
204
209
|
|
|
205
|
-
test("complete-slice prompt references
|
|
210
|
+
test("complete-slice prompt references gsd_slice_complete tool", () => {
|
|
206
211
|
const prompt = readPrompt("complete-slice");
|
|
207
|
-
assert.match(prompt, /
|
|
212
|
+
assert.match(prompt, /gsd_slice_complete/);
|
|
208
213
|
});
|
|
209
214
|
|
|
210
215
|
test("complete-slice prompt does not instruct LLM to toggle checkboxes manually", () => {
|
|
@@ -216,7 +221,7 @@ test("complete-slice prompt instructs writing summary and UAT files before tool
|
|
|
216
221
|
const prompt = readPrompt("complete-slice");
|
|
217
222
|
assert.match(prompt, /\{\{sliceSummaryPath\}\}/);
|
|
218
223
|
assert.match(prompt, /\{\{sliceUatPath\}\}/);
|
|
219
|
-
assert.match(prompt, /
|
|
224
|
+
assert.match(prompt, /gsd_slice_complete/);
|
|
220
225
|
assert.match(prompt, /DB-backed tool is the canonical write path/i);
|
|
221
226
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{sliceSummaryPath\}\}`?/i);
|
|
222
227
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{sliceUatPath\}\}`?/i);
|
|
@@ -398,3 +403,141 @@ test("reactive-execute prompt references tool calls instead of checkbox updates"
|
|
|
398
403
|
assert.doesNotMatch(prompt, /checkbox edits/);
|
|
399
404
|
assert.match(prompt, /completion tool calls/);
|
|
400
405
|
});
|
|
406
|
+
|
|
407
|
+
// ─── Project-shape classifier + 3-or-4-options-with-Other-hatch contract ──
|
|
408
|
+
|
|
409
|
+
test("guided-discuss-project classifies project shape and persists the verdict to PROJECT.md", () => {
|
|
410
|
+
const prompt = readPrompt("guided-discuss-project");
|
|
411
|
+
assert.match(prompt, /Classify project shape/i, "must include the classifier section");
|
|
412
|
+
assert.match(prompt, /`simple`/);
|
|
413
|
+
assert.match(prompt, /`complex`/);
|
|
414
|
+
assert.match(prompt, /Default to `complex` when uncertain/i);
|
|
415
|
+
assert.match(prompt, /## Project Shape/, "must reference the persisted PROJECT.md section");
|
|
416
|
+
assert.match(prompt, /\*\*Complexity:\*\*\s*simple/);
|
|
417
|
+
assert.match(prompt, /\*\*Complexity:\*\*\s*complex/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("guided-discuss prompts require 3-or-4 options plus Other-let-me-discuss in complex mode", () => {
|
|
421
|
+
for (const name of [
|
|
422
|
+
"guided-discuss-project",
|
|
423
|
+
"guided-discuss-milestone",
|
|
424
|
+
"guided-discuss-slice",
|
|
425
|
+
]) {
|
|
426
|
+
const prompt = readPrompt(name);
|
|
427
|
+
assert.match(
|
|
428
|
+
prompt,
|
|
429
|
+
/3 or 4 concrete, researched options/i,
|
|
430
|
+
`${name} must require 3 or 4 grounded options in complex mode`,
|
|
431
|
+
);
|
|
432
|
+
assert.match(
|
|
433
|
+
prompt,
|
|
434
|
+
/"Other — let me discuss"/,
|
|
435
|
+
`${name} must include the "Other — let me discuss" escape hatch`,
|
|
436
|
+
);
|
|
437
|
+
assert.match(
|
|
438
|
+
prompt,
|
|
439
|
+
/grounded in (the |your |)investigation/i,
|
|
440
|
+
`${name} must require options grounded in prior investigation`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("guided-discuss-requirements scopes the 3-or-4-options rule to free-form questions only", () => {
|
|
446
|
+
const prompt = readPrompt("guided-discuss-requirements");
|
|
447
|
+
assert.match(prompt, /3 or 4 concrete, researched options/i);
|
|
448
|
+
assert.match(prompt, /"Other — let me discuss"/);
|
|
449
|
+
// Class-assignment and status questions have fixed enumerations, so the rule must exempt them.
|
|
450
|
+
assert.match(prompt, /class-assignment.*status.*exempt/i);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("downstream discuss prompts read project shape verdict from PROJECT.md", () => {
|
|
454
|
+
for (const name of [
|
|
455
|
+
"guided-discuss-milestone",
|
|
456
|
+
"guided-discuss-requirements",
|
|
457
|
+
"guided-discuss-slice",
|
|
458
|
+
]) {
|
|
459
|
+
const prompt = readPrompt(name);
|
|
460
|
+
assert.match(
|
|
461
|
+
prompt,
|
|
462
|
+
/Project Shape/,
|
|
463
|
+
`${name} must reference Project Shape from PROJECT.md`,
|
|
464
|
+
);
|
|
465
|
+
assert.match(
|
|
466
|
+
prompt,
|
|
467
|
+
/default to `complex`/i,
|
|
468
|
+
`${name} must default to complex when the verdict is missing`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("project template includes the Project Shape section so the verdict has a home", () => {
|
|
474
|
+
const template = readTemplate("project");
|
|
475
|
+
assert.match(template, /## Project Shape/);
|
|
476
|
+
assert.match(template, /\*\*Complexity:\*\*/);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ─── Project shape verdict — end-to-end propagation contract (F7 / #5267) ──
|
|
480
|
+
// The verdict is propagated from discuss-project to downstream stages via
|
|
481
|
+
// PROJECT.md text only, with no parser. These tests pin the round-trip:
|
|
482
|
+
// the format the upstream stage is told to write must be discoverable by
|
|
483
|
+
// the regex pattern the downstream stage is told to look for.
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Render the project.md template with a concrete complexity verdict so we
|
|
487
|
+
* can assert on a realistic PROJECT.md (the placeholder is filled the way
|
|
488
|
+
* an LLM following the prompt would fill it).
|
|
489
|
+
*/
|
|
490
|
+
function renderProjectMd(verdict: "simple" | "complex"): string {
|
|
491
|
+
return readTemplate("project")
|
|
492
|
+
.replace("{{simple | complex}}", verdict)
|
|
493
|
+
.replace("{{one-line rationale citing the signals that decided it}}", "Test fixture rationale.");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
test("project shape verdict survives the discuss-project → discuss-milestone round trip", () => {
|
|
497
|
+
for (const verdict of ["simple", "complex"] as const) {
|
|
498
|
+
const projectMd = renderProjectMd(verdict);
|
|
499
|
+
|
|
500
|
+
// Upstream contract: the PROJECT.md the discuss-project prompt writes
|
|
501
|
+
// must contain the section header and the bolded `**Complexity:** <verdict>`
|
|
502
|
+
// marker that downstream stages are told to grep for.
|
|
503
|
+
assert.match(projectMd, /## Project Shape/, `rendered ${verdict} PROJECT.md must keep the section header`);
|
|
504
|
+
const complexityMarker = new RegExp(`\\*\\*Complexity:\\*\\*\\s*${verdict}\\b`);
|
|
505
|
+
assert.match(
|
|
506
|
+
projectMd,
|
|
507
|
+
complexityMarker,
|
|
508
|
+
`rendered ${verdict} PROJECT.md must expose the bolded Complexity marker the downstream regex looks for`,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Downstream contract: discuss-milestone, discuss-requirements, and
|
|
512
|
+
// discuss-slice must each instruct the LLM to look at the same section
|
|
513
|
+
// header AND the same `**Complexity:**` marker the template writes.
|
|
514
|
+
// Without this, the upstream verdict is silently dropped.
|
|
515
|
+
for (const downstream of ["guided-discuss-milestone", "guided-discuss-requirements", "guided-discuss-slice"]) {
|
|
516
|
+
const prompt = readPrompt(downstream);
|
|
517
|
+
assert.match(
|
|
518
|
+
prompt,
|
|
519
|
+
/## Project Shape/,
|
|
520
|
+
`${downstream} must direct the LLM to the same section header the template writes`,
|
|
521
|
+
);
|
|
522
|
+
assert.match(
|
|
523
|
+
prompt,
|
|
524
|
+
/\*\*Complexity:\*\*/,
|
|
525
|
+
`${downstream} must direct the LLM to the same **Complexity:** marker the template writes`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("downstream discuss prompts default to complex when PROJECT.md lacks the verdict", () => {
|
|
532
|
+
// Safe-by-default: if upstream omits the section (existing projects, LLM
|
|
533
|
+
// drift, future template change), each downstream stage must explicitly
|
|
534
|
+
// fall back to complex so behavior is conservative rather than stuck.
|
|
535
|
+
for (const downstream of ["guided-discuss-milestone", "guided-discuss-requirements", "guided-discuss-slice"]) {
|
|
536
|
+
const prompt = readPrompt(downstream);
|
|
537
|
+
assert.match(
|
|
538
|
+
prompt,
|
|
539
|
+
/default to `complex`/i,
|
|
540
|
+
`${downstream} must default to complex when the upstream verdict is missing`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
@@ -25,12 +25,15 @@ test("register-hooks unlocks milestone depth verification from question id witho
|
|
|
25
25
|
const dir = makeTempDir("manual");
|
|
26
26
|
const originalCwd = process.cwd();
|
|
27
27
|
process.chdir(dir);
|
|
28
|
-
resetWriteGateState();
|
|
28
|
+
resetWriteGateState(dir);
|
|
29
29
|
|
|
30
30
|
t.after(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try {
|
|
32
|
+
resetWriteGateState(dir);
|
|
33
|
+
} finally {
|
|
34
|
+
process.chdir(originalCwd);
|
|
35
|
+
rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<void> | void>>();
|
|
@@ -101,12 +104,15 @@ test("register-hooks clears depth gate when remote (Telegram/Slack/Discord) answ
|
|
|
101
104
|
const dir = makeTempDir("remote");
|
|
102
105
|
const originalCwd = process.cwd();
|
|
103
106
|
process.chdir(dir);
|
|
104
|
-
resetWriteGateState();
|
|
107
|
+
resetWriteGateState(dir);
|
|
105
108
|
|
|
106
109
|
t.after(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
try {
|
|
111
|
+
resetWriteGateState(dir);
|
|
112
|
+
} finally {
|
|
113
|
+
process.chdir(originalCwd);
|
|
114
|
+
rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
110
116
|
});
|
|
111
117
|
|
|
112
118
|
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<void> | void>>();
|
|
@@ -167,12 +173,15 @@ test("register-hooks returns hard blocker when depth question is cancelled", asy
|
|
|
167
173
|
const dir = makeTempDir("cancelled");
|
|
168
174
|
const originalCwd = process.cwd();
|
|
169
175
|
process.chdir(dir);
|
|
170
|
-
resetWriteGateState();
|
|
176
|
+
resetWriteGateState(dir);
|
|
171
177
|
|
|
172
178
|
t.after(() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
try {
|
|
180
|
+
resetWriteGateState(dir);
|
|
181
|
+
} finally {
|
|
182
|
+
process.chdir(originalCwd);
|
|
183
|
+
rmSync(dir, { recursive: true, force: true });
|
|
184
|
+
}
|
|
176
185
|
});
|
|
177
186
|
|
|
178
187
|
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
@@ -222,18 +231,131 @@ test("register-hooks returns hard blocker when depth question is cancelled", asy
|
|
|
222
231
|
patch?.content?.[0]?.text ?? "",
|
|
223
232
|
/Do not infer approval from earlier or prior messages/,
|
|
224
233
|
);
|
|
234
|
+
// Regression for milestone-hang: the cancelled-gate instruction must direct
|
|
235
|
+
// the agent toward the most reliable recovery path — re-calling
|
|
236
|
+
// ask_user_questions with the same gate id. The plain-text path also clears
|
|
237
|
+
// the gate via isExplicitApprovalResponse on the next before_agent_start,
|
|
238
|
+
// but the structured re-ask is more deterministic, so the message points
|
|
239
|
+
// there and avoids the prior dead-end "ask in plain chat, then stop" wording.
|
|
240
|
+
assert.match(
|
|
241
|
+
patch?.content?.[0]?.text ?? "",
|
|
242
|
+
/Re-call ask_user_questions with the same gate question id/,
|
|
243
|
+
"must instruct the agent to re-ask via ask_user_questions",
|
|
244
|
+
);
|
|
245
|
+
assert.doesNotMatch(
|
|
246
|
+
patch?.content?.[0]?.text ?? "",
|
|
247
|
+
/confirm in plain chat, then stop/,
|
|
248
|
+
"must not direct the agent down the prior dead-end plain-chat-and-stop path",
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("register-hooks recovers from a cancelled depth question via re-asked ask_user_questions (milestone-hang regression)", async (t) => {
|
|
253
|
+
const dir = makeTempDir("recovery");
|
|
254
|
+
const originalCwd = process.cwd();
|
|
255
|
+
process.chdir(dir);
|
|
256
|
+
resetWriteGateState(dir);
|
|
257
|
+
|
|
258
|
+
t.after(() => {
|
|
259
|
+
try {
|
|
260
|
+
resetWriteGateState(dir);
|
|
261
|
+
} finally {
|
|
262
|
+
process.chdir(originalCwd);
|
|
263
|
+
rmSync(dir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
268
|
+
const pi = {
|
|
269
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
270
|
+
const existing = handlers.get(event) ?? [];
|
|
271
|
+
existing.push(handler);
|
|
272
|
+
handlers.set(event, existing);
|
|
273
|
+
},
|
|
274
|
+
} as any;
|
|
275
|
+
|
|
276
|
+
registerHooks(pi, []);
|
|
277
|
+
|
|
278
|
+
const questionId = "depth_verification_M001_confirm";
|
|
279
|
+
const questions = [
|
|
280
|
+
{
|
|
281
|
+
id: questionId,
|
|
282
|
+
question: "Did I capture the project correctly?",
|
|
283
|
+
options: [
|
|
284
|
+
{ label: "Yes, you got it (Recommended)" },
|
|
285
|
+
{ label: "Not quite — let me clarify" },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
// 1. Initial ask sets the gate.
|
|
291
|
+
for (const handler of handlers.get("tool_call") ?? []) {
|
|
292
|
+
await handler({ toolName: "ask_user_questions", input: { questions } });
|
|
293
|
+
}
|
|
294
|
+
assert.equal(getPendingGate(), questionId, "initial ask must set the gate");
|
|
295
|
+
|
|
296
|
+
// 2. User cancels (simulates the trap from the screenshot: question never
|
|
297
|
+
// answered through the structured channel). Gate must stay pending.
|
|
298
|
+
for (const handler of handlers.get("tool_result") ?? []) {
|
|
299
|
+
await handler({
|
|
300
|
+
toolName: "ask_user_questions",
|
|
301
|
+
input: { questions },
|
|
302
|
+
details: { cancelled: true, response: null },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
assert.equal(getPendingGate(), questionId, "cancelled response must leave gate pending");
|
|
306
|
+
|
|
307
|
+
// 3. Recovery path: immediately re-call ask_user_questions with the same
|
|
308
|
+
// gate id and identical input. This must not be blocked by the strict
|
|
309
|
+
// duplicate-call loop guard, because the hard-block instruction above
|
|
310
|
+
// tells the agent to do exactly this and not to interleave other tools.
|
|
311
|
+
const reaskBlocks: any[] = [];
|
|
312
|
+
for (const handler of handlers.get("tool_call") ?? []) {
|
|
313
|
+
const result = await handler({ toolName: "ask_user_questions", input: { questions } });
|
|
314
|
+
if (result?.block) reaskBlocks.push(result);
|
|
315
|
+
}
|
|
316
|
+
assert.equal(
|
|
317
|
+
reaskBlocks.length,
|
|
318
|
+
0,
|
|
319
|
+
"immediate identical re-ask must not be blocked by the tool-call loop guard",
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// 4. The re-asked question receives a confirming response, which clears the
|
|
323
|
+
// gate and unlocks the milestone context save.
|
|
324
|
+
for (const handler of handlers.get("tool_result") ?? []) {
|
|
325
|
+
await handler({
|
|
326
|
+
toolName: "ask_user_questions",
|
|
327
|
+
input: { questions },
|
|
328
|
+
details: {
|
|
329
|
+
response: {
|
|
330
|
+
answers: {
|
|
331
|
+
[questionId]: { selected: "Yes, you got it (Recommended)" },
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
assert.equal(getPendingGate(), null, "confirming re-ask must clear the gate");
|
|
339
|
+
assert.equal(
|
|
340
|
+
shouldBlockContextArtifactSave("CONTEXT", "M001").block,
|
|
341
|
+
false,
|
|
342
|
+
"context save must unlock after recovery",
|
|
343
|
+
);
|
|
225
344
|
});
|
|
226
345
|
|
|
227
346
|
test("register-hooks gates MCP ask_user_questions cancellation before requirement saves", async (t) => {
|
|
228
347
|
const dir = makeTempDir("mcp-cancelled");
|
|
229
348
|
const originalCwd = process.cwd();
|
|
230
349
|
process.chdir(dir);
|
|
231
|
-
resetWriteGateState();
|
|
350
|
+
resetWriteGateState(dir);
|
|
232
351
|
|
|
233
352
|
t.after(() => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
353
|
+
try {
|
|
354
|
+
resetWriteGateState(dir);
|
|
355
|
+
} finally {
|
|
356
|
+
process.chdir(originalCwd);
|
|
357
|
+
rmSync(dir, { recursive: true, force: true });
|
|
358
|
+
}
|
|
237
359
|
});
|
|
238
360
|
|
|
239
361
|
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// GSD-2 + Regression tests for missing-worktree warning on resume (M4 fix)
|
|
2
|
+
//
|
|
3
|
+
// When paused-session.json records a worktreePath that no longer exists on disk,
|
|
4
|
+
// the resume path must emit a logWarning("session", ...) describing the situation
|
|
5
|
+
// rather than silently falling back to project-root mode.
|
|
6
|
+
//
|
|
7
|
+
// Strategy: drive the exported _warnIfWorktreeMissingForTest seam directly
|
|
8
|
+
// (mirrors the exact conditional used at the two resume sites in auto.ts),
|
|
9
|
+
// and independently verify the scope fallback via createWorkspace/scopeMilestone
|
|
10
|
+
// as in auto-session-scope.test.ts.
|
|
11
|
+
|
|
12
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
logWarning,
|
|
20
|
+
peekLogs,
|
|
21
|
+
_resetLogs,
|
|
22
|
+
setStderrLoggingEnabled,
|
|
23
|
+
} from "../workflow-logger.ts";
|
|
24
|
+
import { _warnIfWorktreeMissingForTest } from "../auto.ts";
|
|
25
|
+
import { AutoSession } from "../auto/session.ts";
|
|
26
|
+
import { createWorkspace, scopeMilestone } from "../workspace.ts";
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function makeProjectDir(): string {
|
|
31
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-resume-warn-test-")));
|
|
32
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Mirror the rebuildScope() fallback from auto.ts when worktree is missing.
|
|
37
|
+
function applyProjectRootScope(
|
|
38
|
+
s: AutoSession,
|
|
39
|
+
projectDir: string,
|
|
40
|
+
milestoneId: string,
|
|
41
|
+
): void {
|
|
42
|
+
const workspace = createWorkspace(projectDir);
|
|
43
|
+
s.scope = scopeMilestone(workspace, milestoneId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("resume: missing worktree warning emission", () => {
|
|
49
|
+
let projectDir: string;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
projectDir = makeProjectDir();
|
|
53
|
+
_resetLogs();
|
|
54
|
+
setStderrLoggingEnabled(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
setStderrLoggingEnabled(true);
|
|
59
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("logWarning is called when worktreePath is set but directory is missing", () => {
|
|
63
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", "M001-nonexistent");
|
|
64
|
+
// missingPath was never created — existsSync returns false
|
|
65
|
+
|
|
66
|
+
const warned = _warnIfWorktreeMissingForTest(missingPath, "M001");
|
|
67
|
+
|
|
68
|
+
assert.equal(warned, true, "_warnIfWorktreeMissingForTest should return true when path is missing");
|
|
69
|
+
|
|
70
|
+
const logs = peekLogs();
|
|
71
|
+
assert.equal(logs.length, 1, "exactly one warning should be emitted");
|
|
72
|
+
assert.equal(logs[0].severity, "warn");
|
|
73
|
+
assert.equal(logs[0].component, "session");
|
|
74
|
+
assert.ok(
|
|
75
|
+
logs[0].message.includes(missingPath),
|
|
76
|
+
`warning message should include the missing path; got: ${logs[0].message}`,
|
|
77
|
+
);
|
|
78
|
+
assert.ok(
|
|
79
|
+
logs[0].message.includes("missing"),
|
|
80
|
+
"warning message should mention 'missing'",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("logWarning message includes milestone ID", () => {
|
|
85
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", "M042-gone");
|
|
86
|
+
_warnIfWorktreeMissingForTest(missingPath, "M042");
|
|
87
|
+
|
|
88
|
+
const logs = peekLogs();
|
|
89
|
+
assert.equal(logs.length, 1);
|
|
90
|
+
assert.equal(logs[0].context?.milestoneId, "M042");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("logWarning is NOT called when worktreePath is null", () => {
|
|
94
|
+
const warned = _warnIfWorktreeMissingForTest(null, "M001");
|
|
95
|
+
|
|
96
|
+
assert.equal(warned, false);
|
|
97
|
+
assert.equal(peekLogs().length, 0, "no warning when worktreePath is null");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("logWarning is NOT called when worktreePath is undefined", () => {
|
|
101
|
+
const warned = _warnIfWorktreeMissingForTest(undefined, "M001");
|
|
102
|
+
|
|
103
|
+
assert.equal(warned, false);
|
|
104
|
+
assert.equal(peekLogs().length, 0, "no warning when worktreePath is undefined");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("logWarning is NOT called when worktreePath exists on disk", () => {
|
|
108
|
+
const existingWorktree = join(projectDir, ".gsd", "worktrees", "M001");
|
|
109
|
+
mkdirSync(existingWorktree, { recursive: true });
|
|
110
|
+
|
|
111
|
+
const warned = _warnIfWorktreeMissingForTest(existingWorktree, "M001");
|
|
112
|
+
|
|
113
|
+
assert.equal(warned, false, "no warning when path exists");
|
|
114
|
+
assert.equal(peekLogs().length, 0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("warning message mentions project-root fallback action", () => {
|
|
118
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", "M099-deleted");
|
|
119
|
+
_warnIfWorktreeMissingForTest(missingPath, "M099");
|
|
120
|
+
|
|
121
|
+
const logs = peekLogs();
|
|
122
|
+
assert.equal(logs.length, 1);
|
|
123
|
+
assert.ok(
|
|
124
|
+
logs[0].message.includes("project-root mode"),
|
|
125
|
+
"warning should mention project-root mode fallback",
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
logs[0].message.includes("gsd-debug"),
|
|
129
|
+
"warning should suggest /gsd-debug recovery action",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("resume: scope fallback to project-root mode when worktree is missing", () => {
|
|
135
|
+
let s: AutoSession;
|
|
136
|
+
let projectDir: string;
|
|
137
|
+
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
projectDir = makeProjectDir();
|
|
140
|
+
s = new AutoSession();
|
|
141
|
+
_resetLogs();
|
|
142
|
+
setStderrLoggingEnabled(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
setStderrLoggingEnabled(true);
|
|
147
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("scope.workspace.mode is 'project' after fallback from missing worktree", () => {
|
|
151
|
+
const mid = "M001";
|
|
152
|
+
s.originalBasePath = projectDir;
|
|
153
|
+
s.currentMilestoneId = mid;
|
|
154
|
+
|
|
155
|
+
// Simulate auto.ts resume path: worktreePath is set but missing → use projectDir
|
|
156
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", mid);
|
|
157
|
+
_warnIfWorktreeMissingForTest(missingPath, mid);
|
|
158
|
+
|
|
159
|
+
// Fallback: use originalBasePath (project root)
|
|
160
|
+
applyProjectRootScope(s, projectDir, mid);
|
|
161
|
+
|
|
162
|
+
assert.ok(s.scope, "scope should be set after fallback");
|
|
163
|
+
assert.equal(s.scope.workspace.mode, "project");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("scope.milestoneId is preserved after project-root fallback", () => {
|
|
167
|
+
const mid = "M002";
|
|
168
|
+
s.originalBasePath = projectDir;
|
|
169
|
+
s.currentMilestoneId = mid;
|
|
170
|
+
|
|
171
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", mid);
|
|
172
|
+
_warnIfWorktreeMissingForTest(missingPath, mid);
|
|
173
|
+
|
|
174
|
+
applyProjectRootScope(s, projectDir, mid);
|
|
175
|
+
|
|
176
|
+
assert.ok(s.scope, "scope should be set");
|
|
177
|
+
assert.equal(s.scope.milestoneId, mid);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("does not throw when worktree path is missing and scope fallback is applied", () => {
|
|
181
|
+
const mid = "M003";
|
|
182
|
+
s.originalBasePath = projectDir;
|
|
183
|
+
s.currentMilestoneId = mid;
|
|
184
|
+
|
|
185
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", mid);
|
|
186
|
+
|
|
187
|
+
assert.doesNotThrow(() => {
|
|
188
|
+
_warnIfWorktreeMissingForTest(missingPath, mid);
|
|
189
|
+
applyProjectRootScope(s, projectDir, mid);
|
|
190
|
+
}, "resume with missing worktree must not throw");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("warning is emitted once per missing worktree — no double-emission", () => {
|
|
194
|
+
const mid = "M004";
|
|
195
|
+
const missingPath = join(projectDir, ".gsd", "worktrees", mid);
|
|
196
|
+
|
|
197
|
+
_warnIfWorktreeMissingForTest(missingPath, mid);
|
|
198
|
+
|
|
199
|
+
// Simulating the second call as would happen if the resume-re-entry site
|
|
200
|
+
// also fires (e.g. pausedSession and freshStartAssessment both carry the path)
|
|
201
|
+
_warnIfWorktreeMissingForTest(missingPath, mid);
|
|
202
|
+
|
|
203
|
+
const logs = peekLogs();
|
|
204
|
+
// Two calls → two warnings (one per site — consistent with the two sites in auto.ts)
|
|
205
|
+
assert.equal(logs.length, 2, "each call to the seam emits one warning");
|
|
206
|
+
assert.equal(logs[0].component, "session");
|
|
207
|
+
assert.equal(logs[1].component, "session");
|
|
208
|
+
});
|
|
209
|
+
});
|