gsd-pi 2.44.0-dev.62b5d6c → 2.44.0-dev.848dd4c

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 (190) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  3. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  4. package/dist/web/standalone/.next/BUILD_ID +1 -1
  5. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  6. package/dist/web/standalone/.next/build-manifest.json +2 -2
  7. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  9. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.html +1 -1
  25. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  32. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  33. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  34. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  35. package/package.json +1 -1
  36. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  37. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  41. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  43. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  45. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  47. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  48. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  49. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  50. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  51. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  52. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  53. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  54. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  55. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  56. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  57. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  58. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  59. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  60. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  61. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  62. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  63. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  64. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  65. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  66. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  67. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  68. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  69. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  70. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  71. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  72. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  73. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  74. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  75. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  76. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  77. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  78. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  79. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  80. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  81. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  82. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  83. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  84. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  85. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +152 -183
  86. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  87. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  88. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  89. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  90. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  91. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  92. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  93. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  94. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  95. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  96. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  97. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  98. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  99. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  100. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  101. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  102. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  103. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  104. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  105. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  106. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  107. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  108. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  109. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  110. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  111. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  112. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  113. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  114. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  115. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  116. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  117. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  118. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  119. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  120. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  121. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  122. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  123. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  124. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  125. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  126. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  127. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  128. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  129. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  130. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  131. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  138. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  139. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  140. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  141. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  142. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  143. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  144. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  145. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  146. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  147. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  148. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  149. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  150. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  151. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  152. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  153. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  154. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  155. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  156. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  157. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  158. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  159. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  160. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  161. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  162. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  163. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  164. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  165. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  167. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  168. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  169. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  170. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  171. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  173. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  174. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  175. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  176. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  177. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  178. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  180. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  181. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  182. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  183. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  184. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  185. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  186. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  187. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  188. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  189. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_buildManifest.js +0 -0
  190. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_ssgManifest.js +0 -0
@@ -1,13 +1,11 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
1
3
  import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { tmpdir } from "node:os";
4
6
  import { spawnSync } from "node:child_process";
5
7
 
6
8
  import { gsdRoot, _clearGsdRootCache } from "../paths.ts";
7
- import { createTestContext } from "./test-helpers.ts";
8
-
9
- const { assertEq, assertTrue, report } = createTestContext();
10
-
11
9
  /** Create a tmp dir and resolve symlinks + 8.3 short names (macOS /var→/private/var, Windows RUNNER~1→runneradmin). */
12
10
  function tmp(): string {
13
11
  const p = mkdtempSync(join(tmpdir(), "gsd-paths-test-"));
@@ -23,91 +21,78 @@ function initGit(dir: string): void {
23
21
  spawnSync("git", ["commit", "--allow-empty", "-m", "init"], { cwd: dir });
24
22
  }
25
23
 
26
- // ── tests ──────────────────────────────────────────────────────────────────
27
-
28
- {
29
- // Case 1: .gsd exists at basePath — fast path
30
- const root = tmp();
31
- try {
32
- mkdirSync(join(root, ".gsd"));
33
- _clearGsdRootCache();
34
- const result = gsdRoot(root);
35
- assertEq(result, join(root, ".gsd"), "fast path: returns basePath/.gsd");
36
- } finally { cleanup(root); }
37
- }
38
-
39
- {
40
- // Case 2: .gsd exists at git root, cwd is a subdirectory
41
- const root = tmp();
42
- try {
43
- initGit(root);
44
- mkdirSync(join(root, ".gsd"));
45
- const sub = join(root, "src", "deep");
46
- mkdirSync(sub, { recursive: true });
47
- _clearGsdRootCache();
48
- const result = gsdRoot(sub);
49
- assertEq(result, join(root, ".gsd"), "git-root probe: finds .gsd at git root from subdirectory");
50
- } finally { cleanup(root); }
51
- }
24
+ describe('paths', () => {
25
+ test('Case 1: .gsd exists at basePath — fast path', () => {
26
+ const root = tmp();
27
+ try {
28
+ mkdirSync(join(root, ".gsd"));
29
+ _clearGsdRootCache();
30
+ const result = gsdRoot(root);
31
+ assert.deepStrictEqual(result, join(root, ".gsd"), "fast path: returns basePath/.gsd");
32
+ } finally { cleanup(root); }
33
+ });
52
34
 
53
- {
54
- // Case 3: .gsd in an ancestor — walk-up finds it (git repo with no .gsd at root)
55
- const root = tmp();
56
- try {
57
- // Init a git repo so git probe returns root — but put .gsd one level deeper
58
- // to force the walk-up path: root/project/.gsd, cwd = root/project/src/deep
59
- initGit(root);
60
- const project = join(root, "project");
61
- mkdirSync(join(project, ".gsd"), { recursive: true });
62
- const deep = join(project, "src", "deep");
63
- mkdirSync(deep, { recursive: true });
64
- _clearGsdRootCache();
65
- // git probe returns root (no .gsd there), so walk-up takes over and finds project/.gsd
66
- const result = gsdRoot(deep);
67
- assertEq(result, join(project, ".gsd"), "walk-up: finds .gsd in ancestor when git root has none");
68
- } finally { cleanup(root); }
69
- }
35
+ test('Case 2: .gsd exists at git root, cwd is a subdirectory', () => {
36
+ const root = tmp();
37
+ try {
38
+ initGit(root);
39
+ mkdirSync(join(root, ".gsd"));
40
+ const sub = join(root, "src", "deep");
41
+ mkdirSync(sub, { recursive: true });
42
+ _clearGsdRootCache();
43
+ const result = gsdRoot(sub);
44
+ assert.deepStrictEqual(result, join(root, ".gsd"), "git-root probe: finds .gsd at git root from subdirectory");
45
+ } finally { cleanup(root); }
46
+ });
70
47
 
71
- {
72
- // Case 4: .gsd nowhere — fallback returns original basePath/.gsd
73
- // Use an isolated git repo so we fully control the environment above basePath
74
- const root = tmp();
75
- try {
76
- initGit(root); // git root = root, no .gsd anywhere
77
- const sub = join(root, "src");
78
- mkdirSync(sub, { recursive: true });
79
- _clearGsdRootCache();
80
- const result = gsdRoot(sub);
81
- // git probe finds root (no .gsd), walk-up finds nothing fallback = sub/.gsd
82
- assertEq(result, join(sub, ".gsd"), "fallback: returns basePath/.gsd when .gsd not found anywhere");
83
- } finally { cleanup(root); }
84
- }
48
+ test('Case 3: .gsd in an ancestor — walk-up finds it', () => {
49
+ const root = tmp();
50
+ try {
51
+ initGit(root);
52
+ const project = join(root, "project");
53
+ mkdirSync(join(project, ".gsd"), { recursive: true });
54
+ const deep = join(project, "src", "deep");
55
+ mkdirSync(deep, { recursive: true });
56
+ _clearGsdRootCache();
57
+ const result = gsdRoot(deep);
58
+ assert.deepStrictEqual(result, join(project, ".gsd"), "walk-up: finds .gsd in ancestor when git root has none");
59
+ } finally { cleanup(root); }
60
+ });
85
61
 
86
- {
87
- // Case 5: cache — second call returns same value without re-probing
88
- const root = tmp();
89
- try {
90
- mkdirSync(join(root, ".gsd"));
91
- _clearGsdRootCache();
92
- const first = gsdRoot(root);
93
- const second = gsdRoot(root);
94
- assertEq(first, second, "cache: same result returned on second call");
95
- assertTrue(first === second, "cache: identity check (same string)");
96
- } finally { cleanup(root); }
97
- }
62
+ test('Case 4: .gsd nowhere — fallback returns original basePath/.gsd', () => {
63
+ const root = tmp();
64
+ try {
65
+ initGit(root);
66
+ const sub = join(root, "src");
67
+ mkdirSync(sub, { recursive: true });
68
+ _clearGsdRootCache();
69
+ const result = gsdRoot(sub);
70
+ assert.deepStrictEqual(result, join(sub, ".gsd"), "fallback: returns basePath/.gsd when .gsd not found anywhere");
71
+ } finally { cleanup(root); }
72
+ });
98
73
 
99
- {
100
- // Case 6: .gsd at basePath takes precedence over ancestor .gsd
101
- const outer = tmp();
102
- try {
103
- initGit(outer);
104
- mkdirSync(join(outer, ".gsd"));
105
- const inner = join(outer, "nested");
106
- mkdirSync(join(inner, ".gsd"), { recursive: true });
107
- _clearGsdRootCache();
108
- const result = gsdRoot(inner);
109
- assertEq(result, join(inner, ".gsd"), "precedence: nearest .gsd wins over ancestor");
110
- } finally { cleanup(outer); }
111
- }
74
+ test('Case 5: cache — second call returns same value without re-probing', () => {
75
+ const root = tmp();
76
+ try {
77
+ mkdirSync(join(root, ".gsd"));
78
+ _clearGsdRootCache();
79
+ const first = gsdRoot(root);
80
+ const second = gsdRoot(root);
81
+ assert.deepStrictEqual(first, second, "cache: same result returned on second call");
82
+ assert.ok(first === second, "cache: identity check (same string)");
83
+ } finally { cleanup(root); }
84
+ });
112
85
 
113
- report();
86
+ test('Case 6: .gsd at basePath takes precedence over ancestor .gsd', () => {
87
+ const outer = tmp();
88
+ try {
89
+ initGit(outer);
90
+ mkdirSync(join(outer, ".gsd"));
91
+ const inner = join(outer, "nested");
92
+ mkdirSync(join(inner, ".gsd"), { recursive: true });
93
+ _clearGsdRootCache();
94
+ const result = gsdRoot(inner);
95
+ assert.deepStrictEqual(result, join(inner, ".gsd"), "precedence: nearest .gsd wins over ancestor");
96
+ } finally { cleanup(outer); }
97
+ });
98
+ });
@@ -1,9 +1,10 @@
1
1
  // GSD Extension — Hook Engine Tests (Post-Unit, Pre-Dispatch, State Persistence)
2
2
 
3
+ import { describe, test } from 'node:test';
4
+ import assert from 'node:assert/strict';
3
5
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
4
6
  import { join } from "node:path";
5
7
  import { tmpdir } from "node:os";
6
- import { createTestContext } from "./test-helpers.ts";
7
8
  import {
8
9
  checkPostUnitHooks,
9
10
  getActiveHook,
@@ -20,8 +21,6 @@ import {
20
21
  triggerHookManually,
21
22
  } from "../post-unit-hooks.ts";
22
23
 
23
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
24
-
25
24
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
26
25
 
27
26
  function createFixtureBase(): string {
@@ -36,14 +35,14 @@ function createFixtureBase(): string {
36
35
 
37
36
  // ─── resolveHookArtifactPath ───────────────────────────────────────────────
38
37
 
39
- console.log("\n=== resolveHookArtifactPath ===");
40
38
 
41
- {
39
+ describe('post-unit-hooks', () => {
40
+ test('resolveHookArtifactPath', () => {
42
41
  const base = "/project";
43
42
 
44
43
  // Task-level
45
44
  const taskPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-PASS.md");
46
- assertEq(
45
+ assert.deepStrictEqual(
47
46
  taskPath,
48
47
  join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-REVIEW-PASS.md"),
49
48
  "task-level artifact path",
@@ -51,7 +50,7 @@ console.log("\n=== resolveHookArtifactPath ===");
51
50
 
52
51
  // Slice-level
53
52
  const slicePath = resolveHookArtifactPath(base, "M001/S01", "REVIEW-PASS.md");
54
- assertEq(
53
+ assert.deepStrictEqual(
55
54
  slicePath,
56
55
  join(base, ".gsd", "milestones", "M001", "slices", "S01", "REVIEW-PASS.md"),
57
56
  "slice-level artifact path",
@@ -59,129 +58,106 @@ console.log("\n=== resolveHookArtifactPath ===");
59
58
 
60
59
  // Milestone-level
61
60
  const milestonePath = resolveHookArtifactPath(base, "M001", "REVIEW-PASS.md");
62
- assertEq(
61
+ assert.deepStrictEqual(
63
62
  milestonePath,
64
63
  join(base, ".gsd", "milestones", "M001", "REVIEW-PASS.md"),
65
64
  "milestone-level artifact path",
66
65
  );
67
- }
66
+ });
68
67
 
69
68
  // ─── resetHookState ────────────────────────────────────────────────────────
70
-
71
- console.log("\n=== resetHookState ===");
72
-
73
- {
69
+ test('resetHookState', () => {
74
70
  resetHookState();
75
- assertEq(getActiveHook(), null, "no active hook after reset");
76
- assertTrue(!isRetryPending(), "no retry pending after reset");
77
- assertEq(consumeRetryTrigger(), null, "no retry trigger after reset");
78
- }
71
+ assert.deepStrictEqual(getActiveHook(), null, "no active hook after reset");
72
+ assert.ok(!isRetryPending(), "no retry pending after reset");
73
+ assert.deepStrictEqual(consumeRetryTrigger(), null, "no retry trigger after reset");
74
+ });
79
75
 
80
76
  // ─── checkPostUnitHooks with no hooks configured ───────────────────────────
81
-
82
- console.log("\n=== No hooks configured ===");
83
-
84
- {
77
+ test('No hooks configured', () => {
85
78
  resetHookState();
86
79
  const base = createFixtureBase();
87
80
  try {
88
81
  const result = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
89
- assertEq(result, null, "returns null when no hooks configured");
82
+ assert.deepStrictEqual(result, null, "returns null when no hooks configured");
90
83
  } finally {
91
84
  rmSync(base, { recursive: true, force: true });
92
85
  }
93
- }
86
+ });
94
87
 
95
88
  // ─── Hook units don't trigger hooks (no hook-on-hook) ──────────────────────
96
-
97
- console.log("\n=== Hook-on-hook prevention ===");
98
-
99
- {
89
+ test('Hook-on-hook prevention', () => {
100
90
  resetHookState();
101
91
  const base = createFixtureBase();
102
92
  try {
103
93
  const result = checkPostUnitHooks("hook/code-review", "M001/S01/T01", base);
104
- assertEq(result, null, "hook units don't trigger other hooks");
94
+ assert.deepStrictEqual(result, null, "hook units don't trigger other hooks");
105
95
  } finally {
106
96
  rmSync(base, { recursive: true, force: true });
107
97
  }
108
- }
98
+ });
109
99
 
110
100
  // ─── consumeRetryTrigger clears state ──────────────────────────────────────
111
-
112
- console.log("\n=== consumeRetryTrigger clears state ===");
113
-
114
- {
101
+ test('consumeRetryTrigger clears state', () => {
115
102
  resetHookState();
116
- assertEq(consumeRetryTrigger(), null, "no trigger initially");
117
- assertTrue(!isRetryPending(), "no retry initially");
118
- }
103
+ assert.deepStrictEqual(consumeRetryTrigger(), null, "no trigger initially");
104
+ assert.ok(!isRetryPending(), "no retry initially");
105
+ });
119
106
 
120
107
  // ─── Variable substitution in prompts ──────────────────────────────────────
121
-
122
- console.log("\n=== Variable substitution ===");
123
-
124
- {
108
+ test('Variable substitution', () => {
125
109
  const base = "/project";
126
110
 
127
111
  // 3-part ID
128
112
  const path3 = resolveHookArtifactPath(base, "M002/S03/T05", "result.md");
129
- assertTrue(path3.includes("M002"), "3-part ID extracts milestoneId");
130
- assertTrue(path3.includes("S03"), "3-part ID extracts sliceId");
131
- assertTrue(path3.includes("T05"), "3-part ID extracts taskId");
132
- assertTrue(path3.includes("milestones"), "3-part ID includes milestones/ segment");
113
+ assert.ok(path3.includes("M002"), "3-part ID extracts milestoneId");
114
+ assert.ok(path3.includes("S03"), "3-part ID extracts sliceId");
115
+ assert.ok(path3.includes("T05"), "3-part ID extracts taskId");
116
+ assert.ok(path3.includes("milestones"), "3-part ID includes milestones/ segment");
133
117
 
134
118
  // 2-part ID
135
119
  const path2 = resolveHookArtifactPath(base, "M002/S03", "result.md");
136
- assertTrue(path2.includes("M002"), "2-part ID extracts milestoneId");
137
- assertTrue(path2.includes("S03"), "2-part ID extracts sliceId");
138
- assertTrue(path2.includes("milestones"), "2-part ID includes milestones/ segment");
120
+ assert.ok(path2.includes("M002"), "2-part ID extracts milestoneId");
121
+ assert.ok(path2.includes("S03"), "2-part ID extracts sliceId");
122
+ assert.ok(path2.includes("milestones"), "2-part ID includes milestones/ segment");
139
123
 
140
124
  // 1-part ID
141
125
  const path1 = resolveHookArtifactPath(base, "M002", "result.md");
142
- assertTrue(path1.includes("M002"), "1-part ID extracts milestoneId");
143
- assertTrue(path1.includes("milestones"), "1-part ID includes milestones/ segment");
144
- }
126
+ assert.ok(path1.includes("M002"), "1-part ID extracts milestoneId");
127
+ assert.ok(path1.includes("milestones"), "1-part ID includes milestones/ segment");
128
+ });
145
129
 
146
130
  // ═══════════════════════════════════════════════════════════════════════════
147
131
  // Phase 2: Pre-Dispatch Hook Tests
148
132
  // ═══════════════════════════════════════════════════════════════════════════
149
-
150
- console.log("\n=== Pre-dispatch: no hooks configured ===");
151
-
152
- {
133
+ test('Pre-dispatch: no hooks configured', () => {
153
134
  const base = createFixtureBase();
154
135
  try {
155
136
  const result = runPreDispatchHooks("execute-task", "M001/S01/T01", "original prompt", base);
156
- assertEq(result.action, "proceed", "proceeds when no hooks");
157
- assertEq(result.prompt, "original prompt", "prompt unchanged");
158
- assertEq(result.firedHooks.length, 0, "no hooks fired");
137
+ assert.deepStrictEqual(result.action, "proceed", "proceeds when no hooks");
138
+ assert.deepStrictEqual(result.prompt, "original prompt", "prompt unchanged");
139
+ assert.deepStrictEqual(result.firedHooks.length, 0, "no hooks fired");
159
140
  } finally {
160
141
  rmSync(base, { recursive: true, force: true });
161
142
  }
162
- }
163
-
164
- console.log("\n=== Pre-dispatch: hook units bypass ===");
143
+ });
165
144
 
166
- {
145
+ test('Pre-dispatch: hook units bypass', () => {
167
146
  const base = createFixtureBase();
168
147
  try {
169
148
  const result = runPreDispatchHooks("hook/review", "M001/S01/T01", "hook prompt", base);
170
- assertEq(result.action, "proceed", "hook units always proceed");
171
- assertEq(result.prompt, "hook prompt", "hook prompt unchanged");
172
- assertEq(result.firedHooks.length, 0, "no hooks fired for hook units");
149
+ assert.deepStrictEqual(result.action, "proceed", "hook units always proceed");
150
+ assert.deepStrictEqual(result.prompt, "hook prompt", "hook prompt unchanged");
151
+ assert.deepStrictEqual(result.firedHooks.length, 0, "no hooks fired for hook units");
173
152
  } finally {
174
153
  rmSync(base, { recursive: true, force: true });
175
154
  }
176
- }
155
+ });
177
156
 
178
157
  // ═══════════════════════════════════════════════════════════════════════════
179
158
  // Phase 3: State Persistence Tests
180
159
  // ═══════════════════════════════════════════════════════════════════════════
181
-
182
- console.log("\n=== State persistence: persist and restore ===");
183
-
184
- {
160
+ test('State persistence: persist and restore', () => {
185
161
  const base = createFixtureBase();
186
162
  try {
187
163
  resetHookState();
@@ -189,19 +165,17 @@ console.log("\n=== State persistence: persist and restore ===");
189
165
  // Persist empty state
190
166
  persistHookState(base);
191
167
  const filePath = join(base, ".gsd", "hook-state.json");
192
- assertTrue(existsSync(filePath), "hook-state.json created");
168
+ assert.ok(existsSync(filePath), "hook-state.json created");
193
169
 
194
170
  const content = JSON.parse(readFileSync(filePath, "utf-8"));
195
- assertEq(typeof content.savedAt, "string", "savedAt is a string");
196
- assertEq(Object.keys(content.cycleCounts).length, 0, "empty cycle counts");
171
+ assert.deepStrictEqual(typeof content.savedAt, "string", "savedAt is a string");
172
+ assert.deepStrictEqual(Object.keys(content.cycleCounts).length, 0, "empty cycle counts");
197
173
  } finally {
198
174
  rmSync(base, { recursive: true, force: true });
199
175
  }
200
- }
201
-
202
- console.log("\n=== State persistence: restore from disk ===");
176
+ });
203
177
 
204
- {
178
+ test('State persistence: restore from disk', () => {
205
179
  const base = createFixtureBase();
206
180
  try {
207
181
  resetHookState();
@@ -222,16 +196,14 @@ console.log("\n=== State persistence: restore from disk ===");
222
196
  // Verify by persisting and reading back
223
197
  persistHookState(base);
224
198
  const restored = JSON.parse(readFileSync(stateFile, "utf-8"));
225
- assertEq(restored.cycleCounts["review/execute-task/M001/S01/T01"], 2, "cycle count restored for review");
226
- assertEq(restored.cycleCounts["simplify/execute-task/M001/S01/T02"], 1, "cycle count restored for simplify");
199
+ assert.deepStrictEqual(restored.cycleCounts["review/execute-task/M001/S01/T01"], 2, "cycle count restored for review");
200
+ assert.deepStrictEqual(restored.cycleCounts["simplify/execute-task/M001/S01/T02"], 1, "cycle count restored for simplify");
227
201
  } finally {
228
202
  rmSync(base, { recursive: true, force: true });
229
203
  }
230
- }
231
-
232
- console.log("\n=== State persistence: clear ===");
204
+ });
233
205
 
234
- {
206
+ test('State persistence: clear', () => {
235
207
  const base = createFixtureBase();
236
208
  try {
237
209
  resetHookState();
@@ -246,77 +218,65 @@ console.log("\n=== State persistence: clear ===");
246
218
  clearPersistedHookState(base);
247
219
 
248
220
  const cleared = JSON.parse(readFileSync(stateFile, "utf-8"));
249
- assertEq(Object.keys(cleared.cycleCounts).length, 0, "cycle counts cleared");
221
+ assert.deepStrictEqual(Object.keys(cleared.cycleCounts).length, 0, "cycle counts cleared");
250
222
  } finally {
251
223
  rmSync(base, { recursive: true, force: true });
252
224
  }
253
- }
225
+ });
254
226
 
255
- console.log("\n=== State persistence: restore handles missing file ===");
256
-
257
- {
227
+ test('State persistence: restore handles missing file', () => {
258
228
  const base = createFixtureBase();
259
229
  try {
260
230
  resetHookState();
261
231
  // Should not throw
262
232
  restoreHookState(base);
263
- assertEq(getActiveHook(), null, "no active hook after restore from missing file");
233
+ assert.deepStrictEqual(getActiveHook(), null, "no active hook after restore from missing file");
264
234
  } finally {
265
235
  rmSync(base, { recursive: true, force: true });
266
236
  }
267
- }
237
+ });
268
238
 
269
- console.log("\n=== State persistence: restore handles corrupt file ===");
270
-
271
- {
239
+ test('State persistence: restore handles corrupt file', () => {
272
240
  const base = createFixtureBase();
273
241
  try {
274
242
  resetHookState();
275
243
  writeFileSync(join(base, ".gsd", "hook-state.json"), "not json", "utf-8");
276
244
  // Should not throw
277
245
  restoreHookState(base);
278
- assertEq(getActiveHook(), null, "no active hook after corrupt restore");
246
+ assert.deepStrictEqual(getActiveHook(), null, "no active hook after corrupt restore");
279
247
  } finally {
280
248
  rmSync(base, { recursive: true, force: true });
281
249
  }
282
- }
250
+ });
283
251
 
284
252
  // ═══════════════════════════════════════════════════════════════════════════
285
253
  // Phase 3: Hook Status Reporting Tests
286
254
  // ═══════════════════════════════════════════════════════════════════════════
287
-
288
- console.log("\n=== Hook status: no hooks ===");
289
-
290
- {
255
+ test('Hook status: no hooks', () => {
291
256
  resetHookState();
292
257
  const entries = getHookStatus();
293
258
  // No preferences file = no hooks
294
- assertEq(entries.length, 0, "no entries when no hooks configured");
259
+ assert.deepStrictEqual(entries.length, 0, "no entries when no hooks configured");
295
260
 
296
261
  const formatted = formatHookStatus();
297
- assertMatch(formatted, /No hooks configured/, "status message says no hooks");
298
- }
262
+ assert.match(formatted, /No hooks configured/, "status message says no hooks");
263
+ });
299
264
 
300
265
  // ═══════════════════════════════════════════════════════════════════════════
301
266
  // Phase 4: Manual Hook Trigger Tests
302
267
  // ═══════════════════════════════════════════════════════════════════════════
303
-
304
- console.log("\n=== triggerHookManually: hook not found ===");
305
-
306
- {
268
+ test('triggerHookManually: hook not found', () => {
307
269
  resetHookState();
308
270
  const base = createFixtureBase();
309
271
  try {
310
272
  const result = triggerHookManually("nonexistent-hook", "execute-task", "M001/S01/T01", base);
311
- assertEq(result, null, "returns null when hook not found");
273
+ assert.deepStrictEqual(result, null, "returns null when hook not found");
312
274
  } finally {
313
275
  rmSync(base, { recursive: true, force: true });
314
276
  }
315
- }
277
+ });
316
278
 
317
- console.log("\n=== triggerHookManually: with configured hook ===");
318
-
319
- {
279
+ test('triggerHookManually: with configured hook', () => {
320
280
  resetHookState();
321
281
  const base = createFixtureBase();
322
282
  try {
@@ -325,16 +285,16 @@ console.log("\n=== triggerHookManually: with configured hook ===");
325
285
  const result = triggerHookManually("code-review", "execute-task", "M001/S01/T01", base);
326
286
  // Result depends on whether code-review hook is configured in preferences
327
287
  // The function should either return null or a valid HookDispatchResult
328
- assertTrue(result === null || typeof result === "object", "returns null or object");
288
+ assert.ok(result === null || typeof result === "object", "returns null or object");
329
289
  if (result) {
330
- assertEq(result.hookName, "code-review", "hook name in result");
331
- assertEq(result.unitType, "hook/code-review", "unit type is hook-prefixed");
332
- assertEq(result.unitId, "M001/S01/T01", "unit ID preserved");
333
- assertTrue(typeof result.prompt === "string", "prompt is a string");
290
+ assert.deepStrictEqual(result.hookName, "code-review", "hook name in result");
291
+ assert.deepStrictEqual(result.unitType, "hook/code-review", "unit type is hook-prefixed");
292
+ assert.deepStrictEqual(result.unitId, "M001/S01/T01", "unit ID preserved");
293
+ assert.ok(typeof result.prompt === "string", "prompt is a string");
334
294
  }
335
295
  } finally {
336
296
  rmSync(base, { recursive: true, force: true });
337
297
  }
338
- }
298
+ });
339
299
 
340
- report();
300
+ });