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.
Files changed (212) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -0,0 +1,434 @@
1
+ // GSD-2 + Sync-layer scope variants: tests for ByScope wrappers in auto-worktree.ts
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ writeFileSync,
9
+ existsSync,
10
+ realpathSync,
11
+ rmSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
17
+ import {
18
+ syncProjectRootToWorktree,
19
+ syncProjectRootToWorktreeByScope,
20
+ syncStateToProjectRoot,
21
+ syncStateToProjectRootByScope,
22
+ syncGsdStateToWorktree,
23
+ syncGsdStateToWorktreeByScope,
24
+ } from "../auto-worktree.ts";
25
+ // Phase C: reconcilePlanCheckboxesByScope was deleted along with the
26
+ // underlying reconcilePlanCheckboxes (auto-worktree.ts). Worktrees no
27
+ // longer maintain a parallel .gsd/ projection that needs reconciliation.
28
+
29
+ // ─── Helpers ────────────────────────────────────────────────────────────────
30
+
31
+ const MID = "M001-abc123";
32
+
33
+ /**
34
+ * Build a minimal project+worktree layout in a temp dir.
35
+ *
36
+ * Layout:
37
+ * <root>/
38
+ * .gsd/
39
+ * milestones/<MID>/
40
+ * <MID>-CONTEXT.md
41
+ * metrics.json
42
+ * completed-units.json
43
+ * runtime/units/
44
+ * .gsd/worktrees/<MID>/
45
+ * .gsd/ ← worktree-local .gsd projection
46
+ * milestones/<MID>/
47
+ *
48
+ * Returns { projectDir, worktreeDir }.
49
+ */
50
+ function makeProjectAndWorktree(base: string): {
51
+ projectDir: string;
52
+ worktreeDir: string;
53
+ } {
54
+ const projectDir = realpathSync(base);
55
+
56
+ // Project .gsd layout
57
+ mkdirSync(join(projectDir, ".gsd", "milestones", MID), { recursive: true });
58
+ mkdirSync(join(projectDir, ".gsd", "runtime", "units"), { recursive: true });
59
+ writeFileSync(join(projectDir, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), "context");
60
+ writeFileSync(join(projectDir, ".gsd", "metrics.json"), '{"tokens":0}');
61
+ writeFileSync(join(projectDir, ".gsd", "completed-units.json"), "[]");
62
+
63
+ // Worktree directory inside .gsd/worktrees/<MID> so isGsdWorktreePath recognises it
64
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", MID);
65
+ mkdirSync(join(worktreeDir, ".gsd", "milestones", MID), { recursive: true });
66
+ mkdirSync(join(worktreeDir, ".gsd", "runtime", "units"), { recursive: true });
67
+
68
+ return { projectDir, worktreeDir };
69
+ }
70
+
71
+ // ─── Suite: identity check (throws on mismatched workspace) ─────────────────
72
+
73
+ describe("ByScope variants: mismatched-workspace identity assertion", () => {
74
+ let tmpA: string;
75
+ let tmpB: string;
76
+
77
+ beforeEach(() => {
78
+ tmpA = mkdtempSync(join(tmpdir(), "gsd-sync-scope-a-"));
79
+ tmpB = mkdtempSync(join(tmpdir(), "gsd-sync-scope-b-"));
80
+ });
81
+
82
+ afterEach(() => {
83
+ rmSync(tmpA, { recursive: true, force: true });
84
+ rmSync(tmpB, { recursive: true, force: true });
85
+ });
86
+
87
+ test("syncProjectRootToWorktreeByScope throws when identityKeys differ", () => {
88
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
89
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
90
+ const wsA = createWorkspace(tmpA);
91
+ const wsB = createWorkspace(tmpB);
92
+ const scopeA = scopeMilestone(wsA, MID);
93
+ const scopeB = scopeMilestone(wsB, MID);
94
+
95
+ assert.throws(
96
+ () => syncProjectRootToWorktreeByScope(scopeA, scopeB),
97
+ /scope identity mismatch/,
98
+ );
99
+ });
100
+
101
+ test("syncStateToProjectRootByScope throws when identityKeys differ", () => {
102
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
103
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
104
+ const wsA = createWorkspace(tmpA);
105
+ const wsB = createWorkspace(tmpB);
106
+ const scopeA = scopeMilestone(wsA, MID);
107
+ const scopeB = scopeMilestone(wsB, MID);
108
+
109
+ assert.throws(
110
+ () => syncStateToProjectRootByScope(scopeA, scopeB),
111
+ /scope identity mismatch/,
112
+ );
113
+ });
114
+
115
+ test("syncGsdStateToWorktreeByScope throws when identityKeys differ", () => {
116
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
117
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
118
+ const wsA = createWorkspace(tmpA);
119
+ const wsB = createWorkspace(tmpB);
120
+ const scopeA = scopeMilestone(wsA, MID);
121
+ const scopeB = scopeMilestone(wsB, MID);
122
+
123
+ assert.throws(
124
+ () => syncGsdStateToWorktreeByScope(scopeA, scopeB),
125
+ /scope identity mismatch/,
126
+ );
127
+ });
128
+
129
+ // Phase C: reconcilePlanCheckboxesByScope identity-mismatch test
130
+ // removed along with the deleted function.
131
+ });
132
+
133
+ // ─── Suite: same-milestone, same-workspace path identity ────────────────────
134
+
135
+ describe("ByScope variants: same-workspace produces same paths regardless of scope side", () => {
136
+ let tmp: string;
137
+
138
+ beforeEach(() => {
139
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-scope-id-"));
140
+ });
141
+
142
+ afterEach(() => {
143
+ rmSync(tmp, { recursive: true, force: true });
144
+ });
145
+
146
+ test("rootScope.workspace.identityKey equals worktreeScope.workspace.identityKey for same project", () => {
147
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
148
+
149
+ const rootWs = createWorkspace(projectDir);
150
+ const worktreeWs = createWorkspace(worktreeDir);
151
+
152
+ assert.equal(
153
+ rootWs.identityKey,
154
+ worktreeWs.identityKey,
155
+ "both scopes from same project must share identityKey",
156
+ );
157
+ });
158
+
159
+ test("rootScope paths and worktreeScope.workspace.projectRoot resolve to the same project root", () => {
160
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
161
+
162
+ const rootWs = createWorkspace(projectDir);
163
+ const worktreeWs = createWorkspace(worktreeDir);
164
+
165
+ assert.equal(
166
+ rootWs.projectRoot,
167
+ worktreeWs.projectRoot,
168
+ "projectRoot must be identical for root and worktree scopes",
169
+ );
170
+ });
171
+ });
172
+
173
+ // ─── Suite: disk-effect parity (scope variants == legacy path variants) ─────
174
+
175
+ describe("syncProjectRootToWorktreeByScope: disk-effect parity with legacy", () => {
176
+ let tmp1: string;
177
+ let tmp2: string;
178
+
179
+ beforeEach(() => {
180
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-legacy-"));
181
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-scope-"));
182
+ });
183
+
184
+ afterEach(() => {
185
+ rmSync(tmp1, { recursive: true, force: true });
186
+ rmSync(tmp2, { recursive: true, force: true });
187
+ });
188
+
189
+ test("scope variant copies milestone dir into worktree identical to legacy variant", () => {
190
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
191
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
192
+
193
+ // Add a source file in project root milestone dir (not yet in worktree)
194
+ const srcFile = "extra-artifact.md";
195
+ writeFileSync(join(proj1, ".gsd", "milestones", MID, srcFile), "artifact");
196
+ writeFileSync(join(proj2, ".gsd", "milestones", MID, srcFile), "artifact");
197
+
198
+ // Remove destination files so something needs to be copied
199
+ rmSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
200
+ rmSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
201
+
202
+ // Run legacy on tmp1, scope variant on tmp2
203
+ syncProjectRootToWorktree(proj1, wt1, MID);
204
+
205
+ const rootWs2 = createWorkspace(proj2);
206
+ const worktreeWs2 = createWorkspace(wt2);
207
+ const rootScope2 = scopeMilestone(rootWs2, MID);
208
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
209
+ syncProjectRootToWorktreeByScope(rootScope2, worktreeScope2);
210
+
211
+ // Both worktrees should now have the CONTEXT.md
212
+ assert.ok(
213
+ existsSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
214
+ "legacy: CONTEXT.md should be copied into worktree",
215
+ );
216
+ assert.ok(
217
+ existsSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
218
+ "scope: CONTEXT.md should be copied into worktree",
219
+ );
220
+ });
221
+ });
222
+
223
+ describe("syncStateToProjectRootByScope: disk-effect parity with legacy", () => {
224
+ let tmp1: string;
225
+ let tmp2: string;
226
+
227
+ beforeEach(() => {
228
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-legacy-"));
229
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-scope-"));
230
+ });
231
+
232
+ afterEach(() => {
233
+ rmSync(tmp1, { recursive: true, force: true });
234
+ rmSync(tmp2, { recursive: true, force: true });
235
+ });
236
+
237
+ test("scope variant copies metrics.json from worktree to project root identical to legacy variant", () => {
238
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
239
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
240
+
241
+ // Write metrics.json into each worktree .gsd
242
+ const metricsContent = '{"tokens":42}';
243
+ writeFileSync(join(wt1, ".gsd", "metrics.json"), metricsContent);
244
+ writeFileSync(join(wt2, ".gsd", "metrics.json"), metricsContent);
245
+
246
+ // Run legacy on tmp1, scope variant on tmp2
247
+ syncStateToProjectRoot(wt1, proj1, MID);
248
+
249
+ const rootWs2 = createWorkspace(proj2);
250
+ const worktreeWs2 = createWorkspace(wt2);
251
+ const rootScope2 = scopeMilestone(rootWs2, MID);
252
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
253
+ syncStateToProjectRootByScope(worktreeScope2, rootScope2);
254
+
255
+ // Both project roots should now have the updated metrics.json
256
+ assert.ok(
257
+ existsSync(join(proj1, ".gsd", "metrics.json")),
258
+ "legacy: metrics.json should be synced to project root",
259
+ );
260
+ assert.ok(
261
+ existsSync(join(proj2, ".gsd", "metrics.json")),
262
+ "scope: metrics.json should be synced to project root",
263
+ );
264
+ });
265
+ });
266
+
267
+ describe("syncGsdStateToWorktreeByScope: disk-effect parity with legacy", () => {
268
+ let tmp1: string;
269
+ let tmp2: string;
270
+
271
+ beforeEach(() => {
272
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-legacy-"));
273
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-scope-"));
274
+ });
275
+
276
+ afterEach(() => {
277
+ rmSync(tmp1, { recursive: true, force: true });
278
+ rmSync(tmp2, { recursive: true, force: true });
279
+ });
280
+
281
+ test("scope variant syncs root state files into worktree identical to legacy variant", () => {
282
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
283
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
284
+
285
+ // Add a root state file in each project .gsd (not yet in worktree)
286
+ writeFileSync(join(proj1, ".gsd", "DECISIONS.md"), "decisions");
287
+ writeFileSync(join(proj2, ".gsd", "DECISIONS.md"), "decisions");
288
+
289
+ // Run legacy on tmp1, scope variant on tmp2
290
+ syncGsdStateToWorktree(proj1, wt1);
291
+
292
+ const rootWs2 = createWorkspace(proj2);
293
+ const worktreeWs2 = createWorkspace(wt2);
294
+ const rootScope2 = scopeMilestone(rootWs2, MID);
295
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
296
+ syncGsdStateToWorktreeByScope(rootScope2, worktreeScope2);
297
+
298
+ // Both worktrees should now have DECISIONS.md
299
+ assert.ok(
300
+ existsSync(join(wt1, ".gsd", "DECISIONS.md")),
301
+ "legacy: DECISIONS.md should be copied into worktree",
302
+ );
303
+ assert.ok(
304
+ existsSync(join(wt2, ".gsd", "DECISIONS.md")),
305
+ "scope: DECISIONS.md should be copied into worktree",
306
+ );
307
+ });
308
+ });
309
+
310
+ // ─── Suite: direction tests ──────────────────────────────────────────────────
311
+
312
+ describe("sync direction: project→worktree variants only write to worktree side", () => {
313
+ let tmp: string;
314
+
315
+ beforeEach(() => {
316
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-dir-"));
317
+ });
318
+
319
+ afterEach(() => {
320
+ rmSync(tmp, { recursive: true, force: true });
321
+ });
322
+
323
+ test("syncProjectRootToWorktreeByScope: new file appears in worktree, not duplicated to project root", () => {
324
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
325
+
326
+ // New file in project root milestone — not yet in worktree
327
+ const marker = "direction-test.md";
328
+ writeFileSync(join(projectDir, ".gsd", "milestones", MID, marker), "marker");
329
+
330
+ // Remove from worktree so we can detect it being added
331
+ const wtDst = join(worktreeDir, ".gsd", "milestones", MID, marker);
332
+ rmSync(wtDst, { force: true });
333
+
334
+ const rootWs = createWorkspace(projectDir);
335
+ const worktreeWs = createWorkspace(worktreeDir);
336
+ const rootScope = scopeMilestone(rootWs, MID);
337
+ const worktreeScope = scopeMilestone(worktreeWs, MID);
338
+
339
+ syncProjectRootToWorktreeByScope(rootScope, worktreeScope);
340
+
341
+ // File should now be in worktree
342
+ assert.ok(existsSync(wtDst), "marker file should appear in worktree after project→worktree sync");
343
+
344
+ // The original in project root should still be there (not removed)
345
+ assert.ok(
346
+ existsSync(join(projectDir, ".gsd", "milestones", MID, marker)),
347
+ "project root marker file should not be removed",
348
+ );
349
+ });
350
+
351
+ test("syncStateToProjectRootByScope: new file appears in project root from worktree", () => {
352
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
353
+
354
+ // Write a runtime unit file into worktree
355
+ const unitFile = "some-unit-M001.json";
356
+ writeFileSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile), '{"status":"done"}');
357
+
358
+ const rootWs = createWorkspace(projectDir);
359
+ const worktreeWs = createWorkspace(worktreeDir);
360
+ const rootScope = scopeMilestone(rootWs, MID);
361
+ const worktreeScope = scopeMilestone(worktreeWs, MID);
362
+
363
+ syncStateToProjectRootByScope(worktreeScope, rootScope);
364
+
365
+ // Unit file should now be in project root
366
+ assert.ok(
367
+ existsSync(join(projectDir, ".gsd", "runtime", "units", unitFile)),
368
+ "runtime unit file should appear in project root after worktree→root sync",
369
+ );
370
+
371
+ // Worktree side should still have the file (not removed)
372
+ assert.ok(
373
+ existsSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile)),
374
+ "worktree runtime unit file should not be removed",
375
+ );
376
+ });
377
+ });
378
+
379
+ // ─── Suite: milestoneId mismatch guard ───────────────────────────────────────
380
+
381
+ describe("ByScope variants: milestoneId mismatch throws for milestone-aware wrappers", () => {
382
+ let tmp: string;
383
+
384
+ beforeEach(() => {
385
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-mid-mismatch-"));
386
+ });
387
+
388
+ afterEach(() => {
389
+ rmSync(tmp, { recursive: true, force: true });
390
+ });
391
+
392
+ test("syncProjectRootToWorktreeByScope throws when milestoneIds differ", () => {
393
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
394
+ const rootWs = createWorkspace(projectDir);
395
+ const worktreeWs = createWorkspace(worktreeDir);
396
+ // Same workspace identity, different milestoneId
397
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
398
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
399
+
400
+ assert.throws(
401
+ () => syncProjectRootToWorktreeByScope(rootScope, worktreeScope),
402
+ /milestoneId mismatch/,
403
+ );
404
+ });
405
+
406
+ test("syncStateToProjectRootByScope throws when milestoneIds differ", () => {
407
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
408
+ const rootWs = createWorkspace(projectDir);
409
+ const worktreeWs = createWorkspace(worktreeDir);
410
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
411
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
412
+
413
+ assert.throws(
414
+ () => syncStateToProjectRootByScope(worktreeScope, rootScope),
415
+ /milestoneId mismatch/,
416
+ );
417
+ });
418
+
419
+ // Phase C: reconcilePlanCheckboxesByScope milestoneId-mismatch test
420
+ // removed along with the deleted function.
421
+
422
+ test("syncGsdStateToWorktreeByScope does NOT throw when milestoneIds differ (workspace-only wrapper)", () => {
423
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
424
+ const rootWs = createWorkspace(projectDir);
425
+ const worktreeWs = createWorkspace(worktreeDir);
426
+ // Different milestoneIds — syncGsdStateToWorktreeByScope must not guard milestoneId
427
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
428
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
429
+
430
+ assert.doesNotThrow(
431
+ () => syncGsdStateToWorktreeByScope(rootScope, worktreeScope),
432
+ );
433
+ });
434
+ });
@@ -0,0 +1,162 @@
1
+ // GSD-2 + Regression test: teardownAutoWorktree clears activeWorkspace even when process.chdir throws
2
+
3
+ /**
4
+ * Regression (H3 broadened scope): `teardownAutoWorktree` must clear `activeWorkspace`
5
+ * (and therefore `getAutoWorktreeOriginalBase()` / `getActiveAutoWorktreeContext()`)
6
+ * unconditionally — regardless of where in the function body an error occurs.
7
+ *
8
+ * The original H3 fix (d1276b021) wrapped only `removeWorktree(...)` in a
9
+ * try/finally. But `process.chdir(originalBasePath)` at the top of the function
10
+ * can throw a GSDError if the target directory no longer exists. In that case
11
+ * execution exits the function before ever reaching the inner try/finally, leaving
12
+ * `activeWorkspace` stale.
13
+ *
14
+ * The fix: a single outer try/finally wraps the entire teardown body so
15
+ * `setActiveWorkspace(null)` runs regardless of which step throws.
16
+ *
17
+ * Test strategy:
18
+ * 1. Populate the registry via `createAutoWorktree` on a real temp git repo.
19
+ * 2. Delete the repo directory so `process.chdir(originalBasePath)` throws.
20
+ * 3. Assert `teardownAutoWorktree` re-throws (chdir failure still propagates).
21
+ * 4. Assert `getActiveWorkspace()` is null — the broadened finally caught it.
22
+ * 5. Regression: success path still clears activeWorkspace (same guarantee).
23
+ */
24
+
25
+ import { describe, test, beforeEach, afterEach } from "node:test";
26
+ import assert from "node:assert/strict";
27
+ import {
28
+ mkdirSync,
29
+ mkdtempSync,
30
+ writeFileSync,
31
+ rmSync,
32
+ realpathSync,
33
+ existsSync,
34
+ } from "node:fs";
35
+ import { join } from "node:path";
36
+ import { tmpdir } from "node:os";
37
+ import { execFileSync } from "node:child_process";
38
+
39
+ import {
40
+ createAutoWorktree,
41
+ teardownAutoWorktree,
42
+ getAutoWorktreeOriginalBase,
43
+ getActiveAutoWorktreeContext,
44
+ _resetAutoWorktreeOriginalBaseForTests,
45
+ } from "../auto-worktree.ts";
46
+
47
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
48
+
49
+ function git(args: string[], cwd: string): void {
50
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
51
+ }
52
+
53
+ function createTempRepo(): string {
54
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-chdir-fail-")));
55
+ git(["init"], dir);
56
+ git(["config", "user.email", "test@gsd.test"], dir);
57
+ git(["config", "user.name", "Test"], dir);
58
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
59
+ writeFileSync(join(dir, "README.md"), "# test\n");
60
+ git(["add", "."], dir);
61
+ git(["commit", "-m", "init"], dir);
62
+ git(["branch", "-M", "main"], dir);
63
+ return dir;
64
+ }
65
+
66
+ function seedMilestone(repoDir: string, milestoneId: string): void {
67
+ const msDir = join(repoDir, ".gsd", "milestones", milestoneId);
68
+ mkdirSync(msDir, { recursive: true });
69
+ writeFileSync(join(msDir, "CONTEXT.md"), `# ${milestoneId} Context\n`);
70
+ git(["add", "."], repoDir);
71
+ git(["commit", "-m", `add ${milestoneId}`], repoDir);
72
+ }
73
+
74
+ // ─── Tests ───────────────────────────────────────────────────────────────────
75
+
76
+ describe("teardown chdir failure clears registry", () => {
77
+ const savedCwd = process.cwd();
78
+ let repoDir: string;
79
+
80
+ beforeEach(() => {
81
+ _resetAutoWorktreeOriginalBaseForTests();
82
+ process.chdir(savedCwd);
83
+ repoDir = "";
84
+ });
85
+
86
+ afterEach(() => {
87
+ _resetAutoWorktreeOriginalBaseForTests();
88
+ process.chdir(savedCwd);
89
+ if (repoDir && existsSync(repoDir)) {
90
+ rmSync(repoDir, { recursive: true, force: true });
91
+ }
92
+ repoDir = "";
93
+ });
94
+
95
+ // ── chdir failure path (the new coverage) ──────────────────────────────────
96
+
97
+ test("registry is null after teardown throws due to chdir failure (H3 broadened scope)", () => {
98
+ repoDir = createTempRepo();
99
+ seedMilestone(repoDir, "M001");
100
+
101
+ // Populate the registry by entering the worktree
102
+ createAutoWorktree(repoDir, "M001");
103
+
104
+ assert.strictEqual(
105
+ getAutoWorktreeOriginalBase(),
106
+ repoDir,
107
+ "registry is populated after createAutoWorktree",
108
+ );
109
+ assert.notStrictEqual(
110
+ getActiveAutoWorktreeContext(),
111
+ null,
112
+ "context is non-null after createAutoWorktree",
113
+ );
114
+
115
+ // Move back to a safe cwd so we can delete the repo dir
116
+ process.chdir(savedCwd);
117
+
118
+ // Delete the repo directory — process.chdir(repoDir) inside teardown will throw
119
+ const capturedRepoDir = repoDir;
120
+ rmSync(repoDir, { recursive: true, force: true });
121
+ repoDir = ""; // afterEach cleanup no longer needed
122
+
123
+ // teardownAutoWorktree must throw (chdir to deleted dir fails)
124
+ assert.throws(
125
+ () => teardownAutoWorktree(capturedRepoDir, "M001"),
126
+ "teardownAutoWorktree should throw when originalBasePath does not exist",
127
+ );
128
+
129
+ // The broadened outer finally must have cleared the registry despite the throw
130
+ assert.strictEqual(
131
+ getAutoWorktreeOriginalBase(),
132
+ null,
133
+ "getAutoWorktreeOriginalBase() is null after chdir-failure teardown (H3)",
134
+ );
135
+ assert.strictEqual(
136
+ getActiveAutoWorktreeContext(),
137
+ null,
138
+ "getActiveAutoWorktreeContext() is null after chdir-failure teardown (H3)",
139
+ );
140
+ });
141
+
142
+ // ── Success path (regression guard) ───────────────────────────────────────
143
+
144
+ test("registry is null after successful teardown (success path regression)", () => {
145
+ repoDir = createTempRepo();
146
+ seedMilestone(repoDir, "M002");
147
+
148
+ // Confirm baseline
149
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null before entering worktree");
150
+
151
+ createAutoWorktree(repoDir, "M002");
152
+
153
+ assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir, "registry set after createAutoWorktree");
154
+ assert.notStrictEqual(getActiveAutoWorktreeContext(), null, "context non-null after createAutoWorktree");
155
+
156
+ // Normal teardown — finally block must still clear registry
157
+ teardownAutoWorktree(repoDir, "M002");
158
+
159
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null after successful teardown");
160
+ assert.strictEqual(getActiveAutoWorktreeContext(), null, "context null after successful teardown");
161
+ });
162
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * GSD Auto-Worktree -- teardown-cleanup-parity.test.ts
3
+ *
4
+ * Regression test: teardownAutoWorktree (abort path) must call
5
+ * clearProjectRootStateFiles, removing STATE.md, auto.lock, and
6
+ * {MID}-META.json from the project root .gsd/ dir.
7
+ *
8
+ * Prior to the fix these files were left behind on disk after abort teardown.
9
+ */
10
+
11
+ import { describe, test, beforeEach, afterEach } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import {
14
+ mkdirSync,
15
+ mkdtempSync,
16
+ writeFileSync,
17
+ existsSync,
18
+ rmSync,
19
+ realpathSync,
20
+ } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { tmpdir } from "node:os";
23
+ import { execFileSync } from "node:child_process";
24
+
25
+ import { teardownAutoWorktree, _resetAutoWorktreeOriginalBaseForTests } from "../auto-worktree.ts";
26
+
27
+ function git(args: string[], cwd: string): void {
28
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
29
+ }
30
+
31
+ function createTempRepo(): string {
32
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-teardown-parity-")));
33
+ git(["init"], dir);
34
+ git(["config", "user.email", "test@gsd.test"], dir);
35
+ git(["config", "user.name", "Test"], dir);
36
+ writeFileSync(join(dir, "README.md"), "# test\n");
37
+ git(["add", "README.md"], dir);
38
+ git(["commit", "-m", "init"], dir);
39
+ git(["branch", "-M", "main"], dir);
40
+ return dir;
41
+ }
42
+
43
+ describe("teardownAutoWorktree cleanup parity", () => {
44
+ let repoDir: string;
45
+
46
+ beforeEach(() => {
47
+ repoDir = createTempRepo();
48
+ _resetAutoWorktreeOriginalBaseForTests();
49
+ });
50
+
51
+ afterEach(() => {
52
+ rmSync(repoDir, { recursive: true, force: true });
53
+ _resetAutoWorktreeOriginalBaseForTests();
54
+ });
55
+
56
+ test("STATE.md and M001-META.json are removed after abort teardown", () => {
57
+ // Phase C pt 2: auto.lock no longer exists as a file — it migrated
58
+ // to the workers + unit_dispatches tables. clearProjectRootStateFiles
59
+ // still removes STATE.md and {MID}-META.json on teardown.
60
+ const gsdDir = join(repoDir, ".gsd");
61
+ const milestonesDir = join(gsdDir, "milestones", "M001");
62
+ mkdirSync(milestonesDir, { recursive: true });
63
+
64
+ const stateMd = join(gsdDir, "STATE.md");
65
+ const metaJson = join(milestonesDir, "M001-META.json");
66
+
67
+ writeFileSync(stateMd, "# State\nactive\n");
68
+ writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
69
+
70
+ assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
71
+ assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
72
+
73
+ // teardownAutoWorktree may throw when git worktree removal fails
74
+ // (no actual worktree was created), but clearProjectRootStateFiles
75
+ // runs before removeWorktree so the state files must be gone regardless.
76
+ try {
77
+ teardownAutoWorktree(repoDir, "M001");
78
+ } catch {
79
+ // git teardown may fail in a minimal test repo — that is acceptable
80
+ }
81
+
82
+ assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree");
83
+ assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree");
84
+ });
85
+
86
+ test("teardown is non-fatal when state files do not exist", () => {
87
+ // No state files created — teardown should not throw due to missing files
88
+ // (clearProjectRootStateFiles tolerates ENOENT).
89
+ try {
90
+ teardownAutoWorktree(repoDir, "M001");
91
+ } catch {
92
+ // git teardown may fail — acceptable
93
+ }
94
+
95
+ // Reaching here means clearProjectRootStateFiles did not throw for missing files.
96
+ assert.ok(true, "teardown with missing state files did not throw from clearProjectRootStateFiles");
97
+ });
98
+ });