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,125 +1,114 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
1
3
  // gsd-inspect — Tests for /gsd inspect output formatting
2
4
  //
3
5
  // Tests the pure formatInspectOutput function with known data.
4
6
 
5
- import { createTestContext } from './test-helpers.ts';
6
7
  import { formatInspectOutput, type InspectData } from '../commands-inspect.ts';
7
8
 
8
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
9
-
10
- // ── formats output with schema version, counts, and recent entries ──
11
- console.log("# === gsd-inspect: full output formatting ===");
12
- {
13
- const data: InspectData = {
14
- schemaVersion: 2,
15
- counts: { decisions: 12, requirements: 8, artifacts: 3 },
16
- recentDecisions: [
17
- { id: "D012", decision: "Use SQLite for persistence", choice: "node:sqlite with fallback" },
18
- { id: "D011", decision: "Markdown dual-write", choice: "DB-first then regenerate" },
19
- ],
20
- recentRequirements: [
21
- { id: "R015", status: "active", description: "Commands register via pi.registerCommand" },
22
- { id: "R014", status: "active", description: "DB writes use upsert pattern" },
23
- ],
24
- };
25
-
26
- const output = formatInspectOutput(data);
27
-
28
- assertMatch(output, /=== GSD Database Inspect ===/, "contains header");
29
- assertMatch(output, /Schema version: 2/, "contains schema version");
30
- assertMatch(output, /Decisions:\s+12/, "contains decisions count");
31
- assertMatch(output, /Requirements:\s+8/, "contains requirements count");
32
- assertMatch(output, /Artifacts:\s+3/, "contains artifacts count");
33
- assertMatch(output, /Recent decisions:/, "contains recent decisions header");
34
- assertMatch(output, /D012: Use SQLite for persistence → node:sqlite with fallback/, "contains D012 entry");
35
- assertMatch(output, /D011: Markdown dual-write DB-first then regenerate/, "contains D011 entry");
36
- assertMatch(output, /Recent requirements:/, "contains recent requirements header");
37
- assertMatch(output, /R015 \[active\]: Commands register via pi\.registerCommand/, "contains R015 entry");
38
- assertMatch(output, /R014 \[active\]: DB writes use upsert pattern/, "contains R014 entry");
39
- }
40
-
41
- // ── handles zero counts and no recent entries ──
42
- console.log("# === gsd-inspect: empty data ===");
43
- {
44
- const data: InspectData = {
45
- schemaVersion: 1,
46
- counts: { decisions: 0, requirements: 0, artifacts: 0 },
47
- recentDecisions: [],
48
- recentRequirements: [],
49
- };
50
-
51
- const output = formatInspectOutput(data);
52
-
53
- assertMatch(output, /Schema version: 1/, "contains schema version 1");
54
- assertMatch(output, /Decisions:\s+0/, "zero decisions");
55
- assertMatch(output, /Requirements:\s+0/, "zero requirements");
56
- assertMatch(output, /Artifacts:\s+0/, "zero artifacts");
57
- assertTrue(!output.includes("Recent decisions:"), "no recent decisions section when empty");
58
- assertTrue(!output.includes("Recent requirements:"), "no recent requirements section when empty");
59
- }
60
-
61
- // ── handles null schema version ──
62
- console.log("# === gsd-inspect: null schema version ===");
63
- {
64
- const data: InspectData = {
65
- schemaVersion: null,
66
- counts: { decisions: 0, requirements: 0, artifacts: 0 },
67
- recentDecisions: [],
68
- recentRequirements: [],
69
- };
70
-
71
- const output = formatInspectOutput(data);
72
- assertMatch(output, /Schema version: unknown/, "null version shows as unknown");
73
- }
74
-
75
- // ── formats up to 5 recent entries ──
76
- console.log("# === gsd-inspect: five recent entries ===");
77
- {
78
- const data: InspectData = {
79
- schemaVersion: 2,
80
- counts: { decisions: 5, requirements: 5, artifacts: 0 },
81
- recentDecisions: [
82
- { id: "D005", decision: "Dec 5", choice: "C5" },
83
- { id: "D004", decision: "Dec 4", choice: "C4" },
84
- { id: "D003", decision: "Dec 3", choice: "C3" },
85
- { id: "D002", decision: "Dec 2", choice: "C2" },
86
- { id: "D001", decision: "Dec 1", choice: "C1" },
87
- ],
88
- recentRequirements: [
89
- { id: "R005", status: "active", description: "Req 5" },
90
- { id: "R004", status: "done", description: "Req 4" },
91
- { id: "R003", status: "active", description: "Req 3" },
92
- { id: "R002", status: "active", description: "Req 2" },
93
- { id: "R001", status: "done", description: "Req 1" },
94
- ],
95
- };
96
-
97
- const output = formatInspectOutput(data);
98
-
99
- for (let i = 1; i <= 5; i++) {
100
- assertMatch(output, new RegExp(`D00${i}: Dec ${i} → C${i}`), `contains D00${i}`);
101
- }
102
- for (let i = 1; i <= 5; i++) {
103
- assertMatch(output, new RegExp(`R00${i}`), `contains R00${i}`);
104
- }
105
- assertMatch(output, /\[active\]/, "contains active status");
106
- assertMatch(output, /\[done\]/, "contains done status");
107
- }
108
-
109
- // ── output is multiline text (not JSON) ──
110
- console.log("# === gsd-inspect: output format ===");
111
- {
112
- const data: InspectData = {
113
- schemaVersion: 2,
114
- counts: { decisions: 1, requirements: 1, artifacts: 0 },
115
- recentDecisions: [{ id: "D001", decision: "Test", choice: "Yes" }],
116
- recentRequirements: [{ id: "R001", status: "active", description: "Test req" }],
117
- };
118
-
119
- const output = formatInspectOutput(data);
120
- const lines = output.split("\n");
121
- assertTrue(lines.length > 5, "output has multiple lines");
122
- assertTrue(!output.startsWith("{"), "output is not JSON");
123
- }
124
-
125
- report();
9
+ describe('gsd-inspect', () => {
10
+ test('full output formatting', () => {
11
+ const data: InspectData = {
12
+ schemaVersion: 2,
13
+ counts: { decisions: 12, requirements: 8, artifacts: 3 },
14
+ recentDecisions: [
15
+ { id: "D012", decision: "Use SQLite for persistence", choice: "node:sqlite with fallback" },
16
+ { id: "D011", decision: "Markdown dual-write", choice: "DB-first then regenerate" },
17
+ ],
18
+ recentRequirements: [
19
+ { id: "R015", status: "active", description: "Commands register via pi.registerCommand" },
20
+ { id: "R014", status: "active", description: "DB writes use upsert pattern" },
21
+ ],
22
+ };
23
+
24
+ const output = formatInspectOutput(data);
25
+
26
+ assert.match(output, /=== GSD Database Inspect ===/, "contains header");
27
+ assert.match(output, /Schema version: 2/, "contains schema version");
28
+ assert.match(output, /Decisions:\s+12/, "contains decisions count");
29
+ assert.match(output, /Requirements:\s+8/, "contains requirements count");
30
+ assert.match(output, /Artifacts:\s+3/, "contains artifacts count");
31
+ assert.match(output, /Recent decisions:/, "contains recent decisions header");
32
+ assert.match(output, /D012: Use SQLite for persistence → node:sqlite with fallback/, "contains D012 entry");
33
+ assert.match(output, /D011: Markdown dual-write → DB-first then regenerate/, "contains D011 entry");
34
+ assert.match(output, /Recent requirements:/, "contains recent requirements header");
35
+ assert.match(output, /R015 \[active\]: Commands register via pi\.registerCommand/, "contains R015 entry");
36
+ assert.match(output, /R014 \[active\]: DB writes use upsert pattern/, "contains R014 entry");
37
+ });
38
+
39
+ test('empty data', () => {
40
+ const data: InspectData = {
41
+ schemaVersion: 1,
42
+ counts: { decisions: 0, requirements: 0, artifacts: 0 },
43
+ recentDecisions: [],
44
+ recentRequirements: [],
45
+ };
46
+
47
+ const output = formatInspectOutput(data);
48
+
49
+ assert.match(output, /Schema version: 1/, "contains schema version 1");
50
+ assert.match(output, /Decisions:\s+0/, "zero decisions");
51
+ assert.match(output, /Requirements:\s+0/, "zero requirements");
52
+ assert.match(output, /Artifacts:\s+0/, "zero artifacts");
53
+ assert.ok(!output.includes("Recent decisions:"), "no recent decisions section when empty");
54
+ assert.ok(!output.includes("Recent requirements:"), "no recent requirements section when empty");
55
+ });
56
+
57
+ test('null schema version', () => {
58
+ const data: InspectData = {
59
+ schemaVersion: null,
60
+ counts: { decisions: 0, requirements: 0, artifacts: 0 },
61
+ recentDecisions: [],
62
+ recentRequirements: [],
63
+ };
64
+
65
+ const output = formatInspectOutput(data);
66
+ assert.match(output, /Schema version: unknown/, "null version shows as unknown");
67
+ });
68
+
69
+ test('five recent entries', () => {
70
+ const data: InspectData = {
71
+ schemaVersion: 2,
72
+ counts: { decisions: 5, requirements: 5, artifacts: 0 },
73
+ recentDecisions: [
74
+ { id: "D005", decision: "Dec 5", choice: "C5" },
75
+ { id: "D004", decision: "Dec 4", choice: "C4" },
76
+ { id: "D003", decision: "Dec 3", choice: "C3" },
77
+ { id: "D002", decision: "Dec 2", choice: "C2" },
78
+ { id: "D001", decision: "Dec 1", choice: "C1" },
79
+ ],
80
+ recentRequirements: [
81
+ { id: "R005", status: "active", description: "Req 5" },
82
+ { id: "R004", status: "done", description: "Req 4" },
83
+ { id: "R003", status: "active", description: "Req 3" },
84
+ { id: "R002", status: "active", description: "Req 2" },
85
+ { id: "R001", status: "done", description: "Req 1" },
86
+ ],
87
+ };
88
+
89
+ const output = formatInspectOutput(data);
90
+
91
+ for (let i = 1; i <= 5; i++) {
92
+ assert.match(output, new RegExp(`D00${i}: Dec ${i} → C${i}`), `contains D00${i}`);
93
+ }
94
+ for (let i = 1; i <= 5; i++) {
95
+ assert.match(output, new RegExp(`R00${i}`), `contains R00${i}`);
96
+ }
97
+ assert.match(output, /\[active\]/, "contains active status");
98
+ assert.match(output, /\[done\]/, "contains done status");
99
+ });
100
+
101
+ test('output format', () => {
102
+ const data: InspectData = {
103
+ schemaVersion: 2,
104
+ counts: { decisions: 1, requirements: 1, artifacts: 0 },
105
+ recentDecisions: [{ id: "D001", decision: "Test", choice: "Yes" }],
106
+ recentRequirements: [{ id: "R001", status: "active", description: "Test req" }],
107
+ };
108
+
109
+ const output = formatInspectOutput(data);
110
+ const lines = output.split("\n");
111
+ assert.ok(lines.length > 5, "output has multiple lines");
112
+ assert.ok(!output.startsWith("{"), "output is not JSON");
113
+ });
114
+ });
@@ -1,3 +1,5 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
1
3
  // gsd-recover.test.ts — Tests for the `gsd recover` recovery logic.
2
4
  // Verifies: populate DB → clear hierarchy → recover from markdown → state matches.
3
5
 
@@ -22,10 +24,6 @@ import {
22
24
  } from '../gsd-db.ts';
23
25
  import { migrateHierarchyToDb } from '../md-importer.ts';
24
26
  import { deriveStateFromDb, invalidateStateCache } from '../state.ts';
25
- import { createTestContext } from './test-helpers.ts';
26
-
27
- const { assertEq, assertTrue, report } = createTestContext();
28
-
29
27
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
30
28
 
31
29
  function createFixtureBase(): string {
@@ -148,10 +146,8 @@ function clearHierarchyTables(): void {
148
146
 
149
147
  // ─── Tests ────────────────────────────────────────────────────────────────
150
148
 
151
- async function main() {
152
- // ─── Test (a): Full recovery round-trip ─────────────────────────────────
153
- console.log('\n=== recover: full round-trip (populate → clear → recover → verify) ===');
154
- {
149
+ describe('gsd-recover', async () => {
150
+ test('full round-trip (populate, clear, recover, verify)', async () => {
155
151
  const base = createFixtureBase();
156
152
  try {
157
153
  // Set up markdown fixtures
@@ -163,14 +159,14 @@ async function main() {
163
159
  // Step 1: Open DB and populate from markdown
164
160
  openDatabase(':memory:');
165
161
  const counts1 = migrateHierarchyToDb(base);
166
- assertEq(counts1.milestones, 1, 'round-trip: initial migration 1 milestone');
167
- assertEq(counts1.slices, 2, 'round-trip: initial migration 2 slices');
168
- assertTrue(counts1.tasks >= 5, 'round-trip: initial migration at least 5 tasks');
162
+ assert.deepStrictEqual(counts1.milestones, 1, 'round-trip: initial migration - 1 milestone');
163
+ assert.deepStrictEqual(counts1.slices, 2, 'round-trip: initial migration - 2 slices');
164
+ assert.ok(counts1.tasks >= 5, 'round-trip: initial migration - at least 5 tasks');
169
165
 
170
166
  // Step 2: Capture state from DB before clearing
171
167
  invalidateStateCache();
172
168
  const stateBefore = await deriveStateFromDb(base);
173
- assertTrue(stateBefore.activeMilestone !== null, 'round-trip: state before has active milestone');
169
+ assert.ok(stateBefore.activeMilestone !== null, 'round-trip: state before has active milestone');
174
170
  const milestonesBefore = getAllMilestones();
175
171
  const slicesBefore = getMilestoneSlices('M001');
176
172
  const s01TasksBefore = getSliceTasks('M001', 'S01');
@@ -179,30 +175,30 @@ async function main() {
179
175
  // Step 3: Clear hierarchy tables
180
176
  clearHierarchyTables();
181
177
  const milestonesAfterClear = getAllMilestones();
182
- assertEq(milestonesAfterClear.length, 0, 'round-trip: milestones cleared');
178
+ assert.deepStrictEqual(milestonesAfterClear.length, 0, 'round-trip: milestones cleared');
183
179
 
184
180
  // Step 4: Recover from markdown
185
181
  const counts2 = migrateHierarchyToDb(base);
186
- assertEq(counts2.milestones, counts1.milestones, 'round-trip: recovery milestone count matches');
187
- assertEq(counts2.slices, counts1.slices, 'round-trip: recovery slice count matches');
188
- assertEq(counts2.tasks, counts1.tasks, 'round-trip: recovery task count matches');
182
+ assert.deepStrictEqual(counts2.milestones, counts1.milestones, 'round-trip: recovery milestone count matches');
183
+ assert.deepStrictEqual(counts2.slices, counts1.slices, 'round-trip: recovery slice count matches');
184
+ assert.deepStrictEqual(counts2.tasks, counts1.tasks, 'round-trip: recovery task count matches');
189
185
 
190
186
  // Step 5: Verify state matches
191
187
  invalidateStateCache();
192
188
  const stateAfter = await deriveStateFromDb(base);
193
189
 
194
- assertEq(stateAfter.phase, stateBefore.phase, 'round-trip: phase matches');
195
- assertEq(
190
+ assert.deepStrictEqual(stateAfter.phase, stateBefore.phase, 'round-trip: phase matches');
191
+ assert.deepStrictEqual(
196
192
  stateAfter.activeMilestone?.id,
197
193
  stateBefore.activeMilestone?.id,
198
194
  'round-trip: active milestone ID matches',
199
195
  );
200
- assertEq(
196
+ assert.deepStrictEqual(
201
197
  stateAfter.activeSlice?.id,
202
198
  stateBefore.activeSlice?.id,
203
199
  'round-trip: active slice ID matches',
204
200
  );
205
- assertEq(
201
+ assert.deepStrictEqual(
206
202
  stateAfter.activeTask?.id,
207
203
  stateBefore.activeTask?.id,
208
204
  'round-trip: active task ID matches',
@@ -210,32 +206,30 @@ async function main() {
210
206
 
211
207
  // Verify row-level data matches
212
208
  const milestonesAfter = getAllMilestones();
213
- assertEq(milestonesAfter.length, milestonesBefore.length, 'round-trip: milestone row count');
214
- assertEq(milestonesAfter[0]?.id, milestonesBefore[0]?.id, 'round-trip: milestone ID');
215
- assertEq(milestonesAfter[0]?.title, milestonesBefore[0]?.title, 'round-trip: milestone title');
209
+ assert.deepStrictEqual(milestonesAfter.length, milestonesBefore.length, 'round-trip: milestone row count');
210
+ assert.deepStrictEqual(milestonesAfter[0]?.id, milestonesBefore[0]?.id, 'round-trip: milestone ID');
211
+ assert.deepStrictEqual(milestonesAfter[0]?.title, milestonesBefore[0]?.title, 'round-trip: milestone title');
216
212
 
217
213
  const slicesAfter = getMilestoneSlices('M001');
218
- assertEq(slicesAfter.length, slicesBefore.length, 'round-trip: slice row count');
219
- assertEq(slicesAfter[0]?.id, slicesBefore[0]?.id, 'round-trip: S01 ID');
220
- assertEq(slicesAfter[0]?.status, slicesBefore[0]?.status, 'round-trip: S01 status');
221
- assertEq(slicesAfter[1]?.id, slicesBefore[1]?.id, 'round-trip: S02 ID');
214
+ assert.deepStrictEqual(slicesAfter.length, slicesBefore.length, 'round-trip: slice row count');
215
+ assert.deepStrictEqual(slicesAfter[0]?.id, slicesBefore[0]?.id, 'round-trip: S01 ID');
216
+ assert.deepStrictEqual(slicesAfter[0]?.status, slicesBefore[0]?.status, 'round-trip: S01 status');
217
+ assert.deepStrictEqual(slicesAfter[1]?.id, slicesBefore[1]?.id, 'round-trip: S02 ID');
222
218
 
223
219
  const s01TasksAfter = getSliceTasks('M001', 'S01');
224
- assertEq(s01TasksAfter.length, s01TasksBefore.length, 'round-trip: S01 task count');
220
+ assert.deepStrictEqual(s01TasksAfter.length, s01TasksBefore.length, 'round-trip: S01 task count');
225
221
 
226
222
  const s02TasksAfter = getSliceTasks('M001', 'S02');
227
- assertEq(s02TasksAfter.length, s02TasksBefore.length, 'round-trip: S02 task count');
223
+ assert.deepStrictEqual(s02TasksAfter.length, s02TasksBefore.length, 'round-trip: S02 task count');
228
224
 
229
225
  closeDatabase();
230
226
  } finally {
231
227
  closeDatabase();
232
228
  cleanup(base);
233
229
  }
234
- }
230
+ });
235
231
 
236
- // ─── Test (a2): v8 planning columns populated after recovery ───────────
237
- console.log('\n=== recover: v8 planning columns populated ===');
238
- {
232
+ test('v8 planning columns populated', async () => {
239
233
  const base = createFixtureBase();
240
234
  try {
241
235
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
@@ -248,75 +242,70 @@ async function main() {
248
242
 
249
243
  // Milestone planning columns
250
244
  const milestone = getMilestone('M001');
251
- assertTrue(milestone !== null, 'v8: milestone exists');
252
- assertEq(milestone!.vision, 'Test recovery round-trip.', 'v8: milestone vision populated');
253
- assertTrue(milestone!.success_criteria.length >= 2, 'v8: milestone success_criteria has entries');
254
- assertEq(milestone!.success_criteria[0], 'All recovery tests pass', 'v8: first success criterion');
255
- assertTrue(milestone!.boundary_map_markdown.includes('Boundary Map'), 'v8: boundary_map_markdown populated');
256
- assertTrue(milestone!.boundary_map_markdown.includes('S01'), 'v8: boundary_map_markdown has S01');
245
+ assert.ok(milestone !== null, 'v8: milestone exists');
246
+ assert.deepStrictEqual(milestone!.vision, 'Test recovery round-trip.', 'v8: milestone vision populated');
247
+ assert.ok(milestone!.success_criteria.length >= 2, 'v8: milestone success_criteria has entries');
248
+ assert.deepStrictEqual(milestone!.success_criteria[0], 'All recovery tests pass', 'v8: first success criterion');
249
+ assert.ok(milestone!.boundary_map_markdown.includes('Boundary Map'), 'v8: boundary_map_markdown populated');
250
+ assert.ok(milestone!.boundary_map_markdown.includes('S01'), 'v8: boundary_map_markdown has S01');
257
251
 
258
252
  // Tool-only fields left empty per D004
259
- assertEq(milestone!.key_risks.length, 0, 'v8: key_risks left empty (tool-only per D004)');
260
- assertEq(milestone!.requirement_coverage, '', 'v8: requirement_coverage left empty (tool-only per D004)');
253
+ assert.deepStrictEqual(milestone!.key_risks.length, 0, 'v8: key_risks left empty (tool-only per D004)');
254
+ assert.deepStrictEqual(milestone!.requirement_coverage, '', 'v8: requirement_coverage left empty (tool-only per D004)');
261
255
 
262
256
  // Slice planning columns
263
257
  const sliceS01 = getSlice('M001', 'S01');
264
- assertTrue(sliceS01 !== null, 'v8: slice S01 exists');
265
- assertEq(sliceS01!.goal, 'Setup fixtures.', 'v8: S01 goal populated');
258
+ assert.ok(sliceS01 !== null, 'v8: slice S01 exists');
259
+ assert.deepStrictEqual(sliceS01!.goal, 'Setup fixtures.', 'v8: S01 goal populated');
266
260
 
267
261
  const sliceS02 = getSlice('M001', 'S02');
268
- assertTrue(sliceS02 !== null, 'v8: slice S02 exists');
269
- assertEq(sliceS02!.goal, 'Build core.', 'v8: S02 goal populated');
262
+ assert.ok(sliceS02 !== null, 'v8: slice S02 exists');
263
+ assert.deepStrictEqual(sliceS02!.goal, 'Build core.', 'v8: S02 goal populated');
270
264
 
271
265
  // Slice tool-only fields left empty per D004
272
- assertEq(sliceS01!.proof_level, '', 'v8: S01 proof_level left empty (tool-only per D004)');
266
+ assert.deepStrictEqual(sliceS01!.proof_level, '', 'v8: S01 proof_level left empty (tool-only per D004)');
273
267
 
274
- // Task planning columns S01/T01
268
+ // Task planning columns - S01/T01
275
269
  const taskS01T01 = getTask('M001', 'S01', 'T01');
276
- assertTrue(taskS01T01 !== null, 'v8: task S01/T01 exists');
277
- assertTrue(taskS01T01!.files.length >= 2, 'v8: S01/T01 files populated');
278
- assertTrue(taskS01T01!.files.includes('init.ts'), 'v8: S01/T01 files includes init.ts');
279
- assertTrue(taskS01T01!.files.includes('config.ts'), 'v8: S01/T01 files includes config.ts');
280
- assertEq(taskS01T01!.verify, '`node test-init.ts`', 'v8: S01/T01 verify populated');
270
+ assert.ok(taskS01T01 !== null, 'v8: task S01/T01 exists');
271
+ assert.ok(taskS01T01!.files.length >= 2, 'v8: S01/T01 files populated');
272
+ assert.ok(taskS01T01!.files.includes('init.ts'), 'v8: S01/T01 files includes init.ts');
273
+ assert.ok(taskS01T01!.files.includes('config.ts'), 'v8: S01/T01 files includes config.ts');
274
+ assert.deepStrictEqual(taskS01T01!.verify, '`node test-init.ts`', 'v8: S01/T01 verify populated');
281
275
 
282
- // Task planning columns S02/T02
276
+ // Task planning columns - S02/T02
283
277
  const taskS02T02 = getTask('M001', 'S02', 'T02');
284
- assertTrue(taskS02T02 !== null, 'v8: task S02/T02 exists');
285
- assertTrue(taskS02T02!.files.length >= 2, 'v8: S02/T02 files populated');
286
- assertTrue(taskS02T02!.files.includes('test-core.ts'), 'v8: S02/T02 files includes test-core.ts');
287
- assertEq(taskS02T02!.verify, '`npm test`', 'v8: S02/T02 verify populated');
278
+ assert.ok(taskS02T02 !== null, 'v8: task S02/T02 exists');
279
+ assert.ok(taskS02T02!.files.length >= 2, 'v8: S02/T02 files populated');
280
+ assert.ok(taskS02T02!.files.includes('test-core.ts'), 'v8: S02/T02 files includes test-core.ts');
281
+ assert.deepStrictEqual(taskS02T02!.verify, '`npm test`', 'v8: S02/T02 verify populated');
288
282
 
289
- // Task with no Files/Verify — not applicable since all fixtures now have them,
290
- // but confirm a task from S02 has correct data
291
283
  const taskS02T03 = getTask('M001', 'S02', 'T03');
292
- assertTrue(taskS02T03 !== null, 'v8: task S02/T03 exists');
293
- assertTrue(taskS02T03!.files.includes('polish.ts'), 'v8: S02/T03 files includes polish.ts');
294
- assertEq(taskS02T03!.verify, '`node test-polish.ts`', 'v8: S02/T03 verify populated');
284
+ assert.ok(taskS02T03 !== null, 'v8: task S02/T03 exists');
285
+ assert.ok(taskS02T03!.files.includes('polish.ts'), 'v8: S02/T03 files includes polish.ts');
286
+ assert.deepStrictEqual(taskS02T03!.verify, '`node test-polish.ts`', 'v8: S02/T03 verify populated');
295
287
 
296
288
  // Diagnostic: v8 planning columns queryable via SQL
297
289
  const db = _getAdapter()!;
298
290
  const milestoneRow = db.prepare("SELECT vision, success_criteria, boundary_map_markdown FROM milestones WHERE id = 'M001'").get() as any;
299
- assertTrue(milestoneRow.vision.length > 0, 'v8-diag: vision column queryable');
300
- assertTrue(milestoneRow.boundary_map_markdown.length > 0, 'v8-diag: boundary_map_markdown column queryable');
291
+ assert.ok(milestoneRow.vision.length > 0, 'v8-diag: vision column queryable');
292
+ assert.ok(milestoneRow.boundary_map_markdown.length > 0, 'v8-diag: boundary_map_markdown column queryable');
301
293
 
302
294
  const sliceRow = db.prepare("SELECT goal FROM slices WHERE milestone_id = 'M001' AND id = 'S01'").get() as any;
303
- assertTrue(sliceRow.goal.length > 0, 'v8-diag: goal column queryable');
295
+ assert.ok(sliceRow.goal.length > 0, 'v8-diag: goal column queryable');
304
296
 
305
297
  const taskRow = db.prepare("SELECT files, verify FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'").get() as any;
306
- assertTrue(taskRow.files.length > 2, 'v8-diag: files column queryable (JSON array)');
307
- assertTrue(taskRow.verify.length > 0, 'v8-diag: verify column queryable');
298
+ assert.ok(taskRow.files.length > 2, 'v8-diag: files column queryable (JSON array)');
299
+ assert.ok(taskRow.verify.length > 0, 'v8-diag: verify column queryable');
308
300
 
309
301
  closeDatabase();
310
302
  } finally {
311
303
  closeDatabase();
312
304
  cleanup(base);
313
305
  }
314
- }
315
-
306
+ });
316
307
 
317
- // ─── Test (b): Idempotent recovery double recover ────────────────────
318
- console.log('\n=== recover: idempotent — double recovery produces same state ===');
319
- {
308
+ test('idempotent - double recovery produces same state', async () => {
320
309
  const base = createFixtureBase();
321
310
  try {
322
311
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
@@ -337,18 +326,18 @@ async function main() {
337
326
  invalidateStateCache();
338
327
  const state2 = await deriveStateFromDb(base);
339
328
 
340
- assertEq(state2.phase, state1.phase, 'idempotent: phase matches');
341
- assertEq(
329
+ assert.deepStrictEqual(state2.phase, state1.phase, 'idempotent: phase matches');
330
+ assert.deepStrictEqual(
342
331
  state2.activeMilestone?.id,
343
332
  state1.activeMilestone?.id,
344
333
  'idempotent: active milestone matches',
345
334
  );
346
- assertEq(
335
+ assert.deepStrictEqual(
347
336
  state2.activeSlice?.id,
348
337
  state1.activeSlice?.id,
349
338
  'idempotent: active slice matches',
350
339
  );
351
- assertEq(
340
+ assert.deepStrictEqual(
352
341
  state2.activeTask?.id,
353
342
  state1.activeTask?.id,
354
343
  'idempotent: active task matches',
@@ -359,11 +348,9 @@ async function main() {
359
348
  closeDatabase();
360
349
  cleanup(base);
361
350
  }
362
- }
351
+ });
363
352
 
364
- // ─── Test (c): Recovery preserves non-hierarchy data ───────────────────
365
- console.log('\n=== recover: preserves decisions/requirements ===');
366
- {
353
+ test('preserves decisions/requirements', async () => {
367
354
  const base = createFixtureBase();
368
355
  try {
369
356
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
@@ -402,35 +389,33 @@ async function main() {
402
389
 
403
390
  // Verify decisions and requirements survived
404
391
  const decisions = db.prepare('SELECT * FROM decisions').all();
405
- assertEq(decisions.length, 1, 'preserve: decision survives clear');
406
- assertEq((decisions[0] as any).id, 'D001', 'preserve: decision ID intact');
392
+ assert.deepStrictEqual(decisions.length, 1, 'preserve: decision survives clear');
393
+ assert.deepStrictEqual((decisions[0] as any).id, 'D001', 'preserve: decision ID intact');
407
394
 
408
395
  const requirements = db.prepare('SELECT * FROM requirements').all();
409
- assertEq(requirements.length, 1, 'preserve: requirement survives clear');
410
- assertEq((requirements[0] as any).id, 'R001', 'preserve: requirement ID intact');
396
+ assert.deepStrictEqual(requirements.length, 1, 'preserve: requirement survives clear');
397
+ assert.deepStrictEqual((requirements[0] as any).id, 'R001', 'preserve: requirement ID intact');
411
398
 
412
399
  // Recover hierarchy
413
400
  migrateHierarchyToDb(base);
414
401
  const milestones = getAllMilestones();
415
- assertTrue(milestones.length > 0, 'preserve: milestones recovered after clear');
402
+ assert.ok(milestones.length > 0, 'preserve: milestones recovered after clear');
416
403
 
417
404
  // Verify non-hierarchy data still intact after recovery
418
405
  const decisionsAfter = db.prepare('SELECT * FROM decisions').all();
419
- assertEq(decisionsAfter.length, 1, 'preserve: decision still present after recovery');
406
+ assert.deepStrictEqual(decisionsAfter.length, 1, 'preserve: decision still present after recovery');
420
407
 
421
408
  closeDatabase();
422
409
  } finally {
423
410
  closeDatabase();
424
411
  cleanup(base);
425
412
  }
426
- }
413
+ });
427
414
 
428
- // ─── Test (d): Recovery from empty markdown dir ────────────────────────
429
- console.log('\n=== recover: empty milestones dir ===');
430
- {
415
+ test('empty milestones dir', async () => {
431
416
  const base = createFixtureBase();
432
417
  try {
433
- // No milestones written just the empty dir
418
+ // No milestones written - just the empty dir
434
419
  openDatabase(':memory:');
435
420
 
436
421
  // Pre-populate to simulate existing state
@@ -439,24 +424,17 @@ async function main() {
439
424
  // Clear and recover from empty
440
425
  clearHierarchyTables();
441
426
  const counts = migrateHierarchyToDb(base);
442
- assertEq(counts.milestones, 0, 'empty: zero milestones recovered');
443
- assertEq(counts.slices, 0, 'empty: zero slices recovered');
444
- assertEq(counts.tasks, 0, 'empty: zero tasks recovered');
427
+ assert.deepStrictEqual(counts.milestones, 0, 'empty: zero milestones recovered');
428
+ assert.deepStrictEqual(counts.slices, 0, 'empty: zero slices recovered');
429
+ assert.deepStrictEqual(counts.tasks, 0, 'empty: zero tasks recovered');
445
430
 
446
431
  const all = getAllMilestones();
447
- assertEq(all.length, 0, 'empty: no milestones in DB after recovery');
432
+ assert.deepStrictEqual(all.length, 0, 'empty: no milestones in DB after recovery');
448
433
 
449
434
  closeDatabase();
450
435
  } finally {
451
436
  closeDatabase();
452
437
  cleanup(base);
453
438
  }
454
- }
455
-
456
- report();
457
- }
458
-
459
- main().catch((error) => {
460
- console.error(error);
461
- process.exit(1);
439
+ });
462
440
  });