gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -261,71 +261,6 @@ test("verification-gate: each check has durationMs", () => {
261
261
  }
262
262
  });
263
263
 
264
- // ─── Infra Error Tagging Tests ───────────────────────────────────────────────
265
-
266
- test("verification-gate: spawnSync ETIMEDOUT → infraError: true on the check", () => {
267
- const tmp = makeTempDir("vg-etimedout");
268
- try {
269
- // Use a short timeout against a long sleep to guarantee ETIMEDOUT
270
- const result = runVerificationGate({
271
- basePath: tmp,
272
- unitId: "T01",
273
- cwd: tmp,
274
- preferenceCommands: ["sleep 60"],
275
- commandTimeoutMs: 200,
276
- });
277
- assert.equal(result.passed, false);
278
- assert.equal(result.checks.length, 1);
279
- assert.ok(result.checks[0].exitCode !== 0, "should have non-zero exit code");
280
- assert.equal(result.checks[0].infraError, true, "ETIMEDOUT should be tagged as infraError");
281
- } finally {
282
- rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
283
- }
284
- });
285
-
286
- test("verification-gate: real command failure does NOT have infraError", () => {
287
- const tmp = makeTempDir("vg-real-fail");
288
- try {
289
- const result = runVerificationGate({
290
- basePath: tmp,
291
- unitId: "T01",
292
- cwd: tmp,
293
- // Cross-platform: node with --eval flag and no shell-sensitive characters
294
- preferenceCommands: ["node --eval \"process.exitCode=1\""],
295
- });
296
- assert.equal(result.passed, false);
297
- assert.equal(result.checks.length, 1);
298
- assert.equal(result.checks[0].exitCode, 1);
299
- assert.equal(result.checks[0].infraError, undefined, "real failure should not be tagged as infraError");
300
- } finally {
301
- rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
302
- }
303
- });
304
-
305
- test("verification-gate: mixed infra + real failure — only infra check is tagged", () => {
306
- const tmp = makeTempDir("vg-mixed-infra");
307
- try {
308
- // Use a timeout that kills "sleep 60" but lets "node --eval" complete (~80ms).
309
- // The gate applies the same timeout to each command sequentially.
310
- const result = runVerificationGate({
311
- basePath: tmp,
312
- unitId: "T01",
313
- cwd: tmp,
314
- preferenceCommands: ["sleep 60", "node --eval \"process.exitCode=2\""],
315
- commandTimeoutMs: 500,
316
- });
317
- assert.equal(result.passed, false);
318
- assert.equal(result.checks.length, 2);
319
- // First check: ETIMEDOUT → infraError
320
- assert.equal(result.checks[0].infraError, true, "timed-out command should be infraError");
321
- // Second check: real exit 2 → no infraError
322
- assert.equal(result.checks[1].exitCode, 2);
323
- assert.equal(result.checks[1].infraError, undefined, "real failure should not be infraError");
324
- } finally {
325
- rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
326
- }
327
- });
328
-
329
264
  // ─── Preference Validation Tests ─────────────────────────────────────────────
330
265
 
331
266
  test("verification-gate: validatePreferences accepts valid verification keys", () => {
@@ -646,7 +581,7 @@ test("formatFailureContext: formats a single failure with command, exit code, st
646
581
  const result: import("../types.ts").VerificationResult = {
647
582
  passed: false,
648
583
  checks: [
649
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "error: unused var", durationMs: 500, blocking: true },
584
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "error: unused var", durationMs: 500 },
650
585
  ],
651
586
  discoverySource: "preference",
652
587
  timestamp: Date.now(),
@@ -663,9 +598,9 @@ test("formatFailureContext: formats multiple failures", () => {
663
598
  const result: import("../types.ts").VerificationResult = {
664
599
  passed: false,
665
600
  checks: [
666
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint error", durationMs: 100, blocking: true },
667
- { command: "npm run test", exitCode: 2, stdout: "", stderr: "test failure", durationMs: 200, blocking: true },
668
- { command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 50, blocking: true },
601
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint error", durationMs: 100 },
602
+ { command: "npm run test", exitCode: 2, stdout: "", stderr: "test failure", durationMs: 200 },
603
+ { command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 50 },
669
604
  ],
670
605
  discoverySource: "preference",
671
606
  timestamp: Date.now(),
@@ -684,7 +619,7 @@ test("formatFailureContext: truncates stderr longer than 2000 chars", () => {
684
619
  const result: import("../types.ts").VerificationResult = {
685
620
  passed: false,
686
621
  checks: [
687
- { command: "big-err", exitCode: 1, stdout: "", stderr: longStderr, durationMs: 100, blocking: true },
622
+ { command: "big-err", exitCode: 1, stdout: "", stderr: longStderr, durationMs: 100 },
688
623
  ],
689
624
  discoverySource: "preference",
690
625
  timestamp: Date.now(),
@@ -699,8 +634,8 @@ test("formatFailureContext: returns empty string when all checks pass", () => {
699
634
  const result: import("../types.ts").VerificationResult = {
700
635
  passed: true,
701
636
  checks: [
702
- { command: "npm run lint", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
703
- { command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 200, blocking: true },
637
+ { command: "npm run lint", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
638
+ { command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 200 },
704
639
  ],
705
640
  discoverySource: "preference",
706
641
  timestamp: Date.now(),
@@ -728,7 +663,6 @@ test("formatFailureContext: caps total output at 10,000 chars", () => {
728
663
  stdout: "",
729
664
  stderr: "e".repeat(1000), // 1000 chars each, 20 * ~1050 (with formatting) > 10,000
730
665
  durationMs: 100,
731
- blocking: true,
732
666
  });
733
667
  }
734
668
  const result: import("../types.ts").VerificationResult = {
@@ -1143,131 +1077,3 @@ test("dependency-audit: subdirectory package.json does not trigger audit", () =>
1143
1077
  assert.equal(npmAuditCalled, false, "subdirectory dependency files should not trigger audit");
1144
1078
  assert.deepStrictEqual(result, []);
1145
1079
  });
1146
-
1147
- // ─── Non-Blocking Discovery Tests ────────────────────────────────────────────
1148
-
1149
- test("non-blocking: package-json discovered commands failing → result.passed is still true", () => {
1150
- const tmp = makeTempDir("vg-nb-pkg-fail");
1151
- try {
1152
- writeFileSync(
1153
- join(tmp, "package.json"),
1154
- JSON.stringify({ scripts: { lint: "eslint .", test: "vitest" } }),
1155
- );
1156
- // These commands will fail because eslint/vitest don't exist in the temp dir
1157
- const result = runVerificationGate({
1158
- basePath: tmp,
1159
- unitId: "T01",
1160
- cwd: tmp,
1161
- // No preference commands — discovery falls through to package.json
1162
- });
1163
- assert.equal(result.discoverySource, "package-json");
1164
- assert.ok(result.checks.length > 0, "should have discovered package.json checks");
1165
- assert.equal(result.passed, true, "package-json failures should not block the gate");
1166
- for (const check of result.checks) {
1167
- assert.equal(check.blocking, false, "package-json checks should be non-blocking");
1168
- }
1169
- } finally {
1170
- rmSync(tmp, { recursive: true, force: true });
1171
- }
1172
- });
1173
-
1174
- test("non-blocking: preference commands failing → result.passed is false", () => {
1175
- const tmp = makeTempDir("vg-nb-pref-fail");
1176
- try {
1177
- const result = runVerificationGate({
1178
- basePath: tmp,
1179
- unitId: "T01",
1180
- cwd: tmp,
1181
- preferenceCommands: ["sh -c 'exit 1'"],
1182
- });
1183
- assert.equal(result.discoverySource, "preference");
1184
- assert.equal(result.passed, false, "preference failures should block the gate");
1185
- assert.equal(result.checks[0].blocking, true, "preference checks should be blocking");
1186
- } finally {
1187
- rmSync(tmp, { recursive: true, force: true });
1188
- }
1189
- });
1190
-
1191
- test("non-blocking: task-plan commands failing → result.passed is false", () => {
1192
- const tmp = makeTempDir("vg-nb-tp-fail");
1193
- try {
1194
- const result = runVerificationGate({
1195
- basePath: tmp,
1196
- unitId: "T01",
1197
- cwd: tmp,
1198
- taskPlanVerify: "sh -c 'exit 1'",
1199
- });
1200
- assert.equal(result.discoverySource, "task-plan");
1201
- assert.equal(result.passed, false, "task-plan failures should block the gate");
1202
- assert.equal(result.checks[0].blocking, true, "task-plan checks should be blocking");
1203
- } finally {
1204
- rmSync(tmp, { recursive: true, force: true });
1205
- }
1206
- });
1207
-
1208
- test("non-blocking: blocking field is set correctly based on discovery source", () => {
1209
- const tmp = makeTempDir("vg-nb-field");
1210
- try {
1211
- // preference → blocking
1212
- const prefResult = runVerificationGate({
1213
- basePath: tmp,
1214
- unitId: "T01",
1215
- cwd: tmp,
1216
- preferenceCommands: ["echo ok"],
1217
- });
1218
- assert.equal(prefResult.checks[0].blocking, true);
1219
-
1220
- // task-plan → blocking
1221
- const tpResult = runVerificationGate({
1222
- basePath: tmp,
1223
- unitId: "T01",
1224
- cwd: tmp,
1225
- taskPlanVerify: "echo ok",
1226
- });
1227
- assert.equal(tpResult.checks[0].blocking, true);
1228
-
1229
- // package-json → non-blocking
1230
- writeFileSync(
1231
- join(tmp, "package.json"),
1232
- JSON.stringify({ scripts: { test: "echo ok" } }),
1233
- );
1234
- const pkgResult = runVerificationGate({
1235
- basePath: tmp,
1236
- unitId: "T01",
1237
- cwd: tmp,
1238
- });
1239
- assert.equal(pkgResult.checks[0].blocking, false);
1240
- } finally {
1241
- rmSync(tmp, { recursive: true, force: true });
1242
- }
1243
- });
1244
-
1245
- test("non-blocking: formatFailureContext only includes blocking failures", () => {
1246
- const result: import("../types.ts").VerificationResult = {
1247
- passed: true,
1248
- checks: [
1249
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint warning", durationMs: 100, blocking: false },
1250
- { command: "npm run test", exitCode: 1, stdout: "", stderr: "test error", durationMs: 200, blocking: true },
1251
- { command: "npm run typecheck", exitCode: 1, stdout: "", stderr: "type error", durationMs: 50, blocking: false },
1252
- ],
1253
- discoverySource: "preference",
1254
- timestamp: Date.now(),
1255
- };
1256
- const output = formatFailureContext(result);
1257
- assert.ok(output.includes("`npm run test`"), "should include blocking failure");
1258
- assert.ok(!output.includes("npm run lint"), "should not include non-blocking failure");
1259
- assert.ok(!output.includes("npm run typecheck"), "should not include non-blocking failure");
1260
- });
1261
-
1262
- test("non-blocking: formatFailureContext returns empty when only non-blocking failures exist", () => {
1263
- const result: import("../types.ts").VerificationResult = {
1264
- passed: true,
1265
- checks: [
1266
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint warning", durationMs: 100, blocking: false },
1267
- { command: "npm run test", exitCode: 1, stdout: "", stderr: "test warning", durationMs: 200, blocking: false },
1268
- ],
1269
- discoverySource: "package-json",
1270
- timestamp: Date.now(),
1271
- };
1272
- assert.equal(formatFailureContext(result), "", "should return empty when only non-blocking failures");
1273
- });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * worktree-db-integration.test.ts
3
+ *
4
+ * Integration tests for the worktree DB copy and reconcile hooks.
5
+ * Uses real temp git repos and real SQLite databases.
6
+ *
7
+ * Test cases:
8
+ * 1. Copy: createAutoWorktree seeds .gsd/gsd.db into the worktree when main has one
9
+ * 2. Copy-skip: createAutoWorktree silently skips when main has no gsd.db
10
+ * 3. Reconcile: reconcileWorktreeDb merges worktree rows into main DB
11
+ * 4. Reconcile-skip: reconcileWorktreeDb is non-fatal when both paths are nonexistent
12
+ * 5. Failure path: reconcileWorktreeDb emits to stderr on open failure (observable)
13
+ */
14
+
15
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+ import { execSync } from "node:child_process";
19
+
20
+ import { createAutoWorktree } from "../auto-worktree.ts";
21
+ import { worktreePath } from "../worktree-manager.ts";
22
+ import {
23
+ copyWorktreeDb,
24
+ reconcileWorktreeDb,
25
+ openDatabase,
26
+ closeDatabase,
27
+ upsertDecision,
28
+ getActiveDecisions,
29
+ isDbAvailable,
30
+ } from "../gsd-db.ts";
31
+
32
+ import { createTestContext } from "./test-helpers.ts";
33
+
34
+ const { assertEq, assertTrue, report } = createTestContext();
35
+
36
+ function run(command: string, cwd: string): string {
37
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
38
+ }
39
+
40
+ function createTempRepo(): string {
41
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-db-int-test-")));
42
+ run("git init", dir);
43
+ run("git config user.email test@test.com", dir);
44
+ run("git config user.name Test", dir);
45
+ writeFileSync(join(dir, "README.md"), "# test\n");
46
+ run("git add .", dir);
47
+ run("git commit -m init", dir);
48
+ run("git branch -M main", dir);
49
+ return dir;
50
+ }
51
+
52
+ async function main(): Promise<void> {
53
+ const savedCwd = process.cwd();
54
+ const tempDirs: string[] = [];
55
+
56
+ function makeTempDir(): string {
57
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-db-int-")));
58
+ tempDirs.push(dir);
59
+ return dir;
60
+ }
61
+
62
+ try {
63
+
64
+ // ─── Test 1: copy on worktree creation ───────────────────────────
65
+ console.log("\n=== Test 1: copy on worktree creation ===");
66
+ {
67
+ const tempDir = createTempRepo();
68
+ tempDirs.push(tempDir);
69
+
70
+ // Seed a gsd.db in the main repo
71
+ const gsdDir = join(tempDir, ".gsd");
72
+ mkdirSync(gsdDir, { recursive: true });
73
+ const mainDbPath = join(gsdDir, "gsd.db");
74
+ openDatabase(mainDbPath);
75
+ closeDatabase();
76
+
77
+ // Commit so createAutoWorktree can copy planning artifacts
78
+ run("git add .", tempDir);
79
+ run('git commit -m "add gsd dir"', tempDir);
80
+
81
+ // createAutoWorktree should copy the DB into the worktree
82
+ const wtPath = createAutoWorktree(tempDir, "M004");
83
+
84
+ const worktreeDbPath = join(worktreePath(tempDir, "M004"), ".gsd", "gsd.db");
85
+ assertTrue(
86
+ existsSync(worktreeDbPath),
87
+ "gsd.db exists in worktree .gsd after createAutoWorktree",
88
+ );
89
+
90
+ // Restore cwd for next test
91
+ process.chdir(savedCwd);
92
+ }
93
+
94
+ // ─── Test 2: copy skip when no source DB ─────────────────────────
95
+ console.log("\n=== Test 2: copy skip when no source DB ===");
96
+ {
97
+ const tempDir = createTempRepo();
98
+ tempDirs.push(tempDir);
99
+
100
+ // No gsd.db — just a bare repo
101
+ let threw = false;
102
+ let wtPath: string | null = null;
103
+ try {
104
+ wtPath = createAutoWorktree(tempDir, "M004");
105
+ } catch (err) {
106
+ threw = true;
107
+ console.error(" Unexpected throw:", err);
108
+ }
109
+
110
+ assertTrue(!threw, "createAutoWorktree does not throw when no source DB");
111
+
112
+ const worktreeDbPath = join(worktreePath(tempDir, "M004"), ".gsd", "gsd.db");
113
+ assertTrue(
114
+ !existsSync(worktreeDbPath),
115
+ "gsd.db is absent in worktree when source had none",
116
+ );
117
+
118
+ process.chdir(savedCwd);
119
+ }
120
+
121
+ // ─── Test 3: reconcile inserts worktree rows into main ───────────
122
+ console.log("\n=== Test 3: reconcile merges worktree rows into main ===");
123
+ {
124
+ const mainDbPath = join(makeTempDir(), "main.db");
125
+ const worktreeDbPath = join(makeTempDir(), "wt.db");
126
+
127
+ // Seed main DB (empty schema)
128
+ openDatabase(mainDbPath);
129
+ closeDatabase();
130
+
131
+ // Seed worktree DB with one decision
132
+ openDatabase(worktreeDbPath);
133
+ upsertDecision({
134
+ id: "D-WT-001",
135
+ when_context: "integration test",
136
+ scope: "test",
137
+ decision: "use reconcile",
138
+ choice: "reconcile on merge",
139
+ rationale: "test coverage",
140
+ revisable: "no",
141
+ superseded_by: null,
142
+ });
143
+ closeDatabase();
144
+
145
+ // Reconcile worktree → main
146
+ const result = reconcileWorktreeDb(mainDbPath, worktreeDbPath);
147
+ assertTrue(result.decisions >= 1, "reconcile reports at least 1 decision merged");
148
+
149
+ // Open main DB and verify the row is present
150
+ openDatabase(mainDbPath);
151
+ const decisions = getActiveDecisions();
152
+ closeDatabase();
153
+
154
+ const found = decisions.some((d) => d.id === "D-WT-001");
155
+ assertTrue(found, "worktree decision D-WT-001 present in main DB after reconcile");
156
+ }
157
+
158
+ // ─── Test 4: reconcile non-fatal when both paths nonexistent ─────
159
+ console.log("\n=== Test 4: reconcile non-fatal on nonexistent paths ===");
160
+ {
161
+ let threw = false;
162
+ try {
163
+ reconcileWorktreeDb("/nonexistent/path/gsd.db", "/also/nonexistent/gsd.db");
164
+ } catch {
165
+ threw = true;
166
+ }
167
+ assertTrue(!threw, "reconcileWorktreeDb does not throw when worktree DB is absent");
168
+ }
169
+
170
+ // ─── Test 5: failure path observable via stderr (diagnostic) ─────
171
+ // reconcileWorktreeDb emits to stderr on reconciliation failures.
172
+ // We can't easily intercept stderr in this test harness, but we verify
173
+ // that the function returns the zero-result shape (not undefined/throws)
174
+ // when the worktree DB is missing — confirming the failure path is non-fatal
175
+ // and returns a structured result.
176
+ console.log("\n=== Test 5: reconcile returns zero-shape when worktree DB absent ===");
177
+ {
178
+ const mainDbPath = join(makeTempDir(), "main2.db");
179
+ openDatabase(mainDbPath);
180
+ closeDatabase();
181
+
182
+ const result = reconcileWorktreeDb(mainDbPath, "/definitely/does/not/exist.db");
183
+ assertEq(result.decisions, 0, "decisions is 0 when worktree DB absent");
184
+ assertEq(result.requirements, 0, "requirements is 0 when worktree DB absent");
185
+ assertEq(result.artifacts, 0, "artifacts is 0 when worktree DB absent");
186
+ assertEq(result.conflicts.length, 0, "conflicts is empty when worktree DB absent");
187
+ }
188
+
189
+ } finally {
190
+ // Always restore cwd
191
+ process.chdir(savedCwd);
192
+ // Ensure DB is closed
193
+ if (isDbAvailable()) closeDatabase();
194
+ // Remove all temp dirs
195
+ for (const dir of tempDirs) {
196
+ if (existsSync(dir)) {
197
+ rmSync(dir, { recursive: true, force: true });
198
+ }
199
+ }
200
+ }
201
+
202
+ report();
203
+ }
204
+
205
+ main();