gsd-pi 2.54.0-dev.e1efc1a → 2.55.0-dev.9ec7cdf

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 (201) hide show
  1. package/dist/cli.js +19 -19
  2. package/dist/headless-ui.d.ts +27 -1
  3. package/dist/headless-ui.js +203 -13
  4. package/dist/headless.js +60 -3
  5. package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +2 -2
  6. package/dist/resources/extensions/bg-shell/utilities.js +34 -5
  7. package/dist/resources/extensions/gsd/auto/phases.js +19 -3
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +17 -1
  10. package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
  11. package/dist/resources/extensions/gsd/auto-start.js +12 -5
  12. package/dist/resources/extensions/gsd/auto-worktree.js +39 -14
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +5 -1
  14. package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +18 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -5
  16. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +20 -0
  17. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +15 -1
  19. package/dist/resources/extensions/gsd/crash-recovery.js +2 -2
  20. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +413 -0
  21. package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -1
  22. package/dist/resources/extensions/gsd/session-lock.js +46 -12
  23. package/dist/resources/extensions/gsd/skill-health.js +2 -2
  24. package/dist/resources/extensions/gsd/visualizer-overlay.js +3 -3
  25. package/dist/resources/extensions/shared/format-utils.js +1 -1
  26. package/dist/resources/extensions/subagent/worker-registry.js +2 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  29. package/dist/web/standalone/.next/build-manifest.json +3 -3
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  32. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  34. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/index.html +1 -1
  94. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  95. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  96. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  97. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  102. package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
  103. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/dist/web/standalone/.next/static/chunks/6502.2305d0afd2385711.js +9 -0
  109. package/dist/web/standalone/.next/static/chunks/app/{page-b950e4e384cc62b3.js → page-0c485498795110d6.js} +1 -1
  110. package/dist/web/standalone/.next/static/chunks/{webpack-bca0e732db0dcec3.js → webpack-4332cbd5dd1be584.js} +1 -1
  111. package/package.json +6 -4
  112. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -1
  113. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +2 -0
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +14 -2
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  119. package/packages/pi-coding-agent/package.json +1 -1
  120. package/packages/pi-coding-agent/src/core/model-registry.ts +1 -1
  121. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -2
  122. package/pkg/package.json +1 -1
  123. package/scripts/ensure-workspace-builds.cjs +45 -41
  124. package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +2 -2
  125. package/src/resources/extensions/bg-shell/utilities.ts +39 -4
  126. package/src/resources/extensions/gsd/auto/phases.ts +25 -4
  127. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  128. package/src/resources/extensions/gsd/auto-model-selection.ts +21 -1
  129. package/src/resources/extensions/gsd/auto-prompts.ts +15 -0
  130. package/src/resources/extensions/gsd/auto-start.ts +13 -5
  131. package/src/resources/extensions/gsd/auto-worktree.ts +46 -13
  132. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +5 -4
  133. package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +53 -0
  134. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -6
  135. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +24 -0
  136. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  137. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +19 -1
  138. package/src/resources/extensions/gsd/crash-recovery.ts +2 -3
  139. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +497 -0
  140. package/src/resources/extensions/gsd/parallel-orchestrator.ts +6 -1
  141. package/src/resources/extensions/gsd/session-lock.ts +46 -12
  142. package/src/resources/extensions/gsd/skill-health.ts +2 -2
  143. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +139 -0
  144. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +28 -0
  145. package/src/resources/extensions/gsd/tests/{all-milestones-complete-merge.test.ts → integration/all-milestones-complete-merge.test.ts} +3 -3
  146. package/src/resources/extensions/gsd/tests/{atomic-task-closeout.test.ts → integration/atomic-task-closeout.test.ts} +1 -1
  147. package/src/resources/extensions/gsd/tests/{auto-preflight.test.ts → integration/auto-preflight.test.ts} +1 -1
  148. package/src/resources/extensions/gsd/tests/{auto-recovery.test.ts → integration/auto-recovery.test.ts} +7 -7
  149. package/src/resources/extensions/gsd/tests/{auto-secrets-gate.test.ts → integration/auto-secrets-gate.test.ts} +2 -2
  150. package/src/resources/extensions/gsd/tests/{auto-stash-merge.test.ts → integration/auto-stash-merge.test.ts} +3 -3
  151. package/src/resources/extensions/gsd/tests/{auto-worktree-milestone-merge.test.ts → integration/auto-worktree-milestone-merge.test.ts} +4 -4
  152. package/src/resources/extensions/gsd/tests/{auto-worktree.test.ts → integration/auto-worktree.test.ts} +5 -5
  153. package/src/resources/extensions/gsd/tests/{continue-here.test.ts → integration/continue-here.test.ts} +3 -3
  154. package/src/resources/extensions/gsd/tests/{doctor-completion-deferral.test.ts → integration/doctor-completion-deferral.test.ts} +1 -1
  155. package/src/resources/extensions/gsd/tests/{doctor-delimiter-fix.test.ts → integration/doctor-delimiter-fix.test.ts} +1 -1
  156. package/src/resources/extensions/gsd/tests/{doctor-enhancements.test.ts → integration/doctor-enhancements.test.ts} +3 -3
  157. package/src/resources/extensions/gsd/tests/{doctor-environment-worktree.test.ts → integration/doctor-environment-worktree.test.ts} +1 -1
  158. package/src/resources/extensions/gsd/tests/{doctor-environment.test.ts → integration/doctor-environment.test.ts} +1 -1
  159. package/src/resources/extensions/gsd/tests/{doctor-fixlevel.test.ts → integration/doctor-fixlevel.test.ts} +2 -2
  160. package/src/resources/extensions/gsd/tests/{doctor-git.test.ts → integration/doctor-git.test.ts} +1 -1
  161. package/src/resources/extensions/gsd/tests/{doctor-proactive.test.ts → integration/doctor-proactive.test.ts} +1 -1
  162. package/src/resources/extensions/gsd/tests/{doctor-roadmap-summary-atomicity.test.ts → integration/doctor-roadmap-summary-atomicity.test.ts} +1 -1
  163. package/src/resources/extensions/gsd/tests/{doctor-runtime.test.ts → integration/doctor-runtime.test.ts} +1 -1
  164. package/src/resources/extensions/gsd/tests/{doctor.test.ts → integration/doctor.test.ts} +1 -1
  165. package/src/resources/extensions/gsd/tests/{e2e-workflow-pipeline-integration.test.ts → integration/e2e-workflow-pipeline-integration.test.ts} +5 -5
  166. package/src/resources/extensions/gsd/tests/{feature-branch-lifecycle-integration.test.ts → integration/feature-branch-lifecycle-integration.test.ts} +4 -4
  167. package/src/resources/extensions/gsd/tests/{git-locale.test.ts → integration/git-locale.test.ts} +4 -4
  168. package/src/resources/extensions/gsd/tests/{git-self-heal.test.ts → integration/git-self-heal.test.ts} +1 -1
  169. package/src/resources/extensions/gsd/tests/{git-service.test.ts → integration/git-service.test.ts} +4 -4
  170. package/src/resources/extensions/gsd/tests/{gitignore-tracked-gsd.test.ts → integration/gitignore-tracked-gsd.test.ts} +2 -2
  171. package/src/resources/extensions/gsd/tests/{idle-recovery.test.ts → integration/idle-recovery.test.ts} +3 -3
  172. package/src/resources/extensions/gsd/tests/{inherited-repo-home-dir.test.ts → integration/inherited-repo-home-dir.test.ts} +1 -1
  173. package/src/resources/extensions/gsd/tests/{integration-lifecycle.test.ts → integration/integration-lifecycle.test.ts} +4 -4
  174. package/src/resources/extensions/gsd/tests/{integration-mixed-milestones.test.ts → integration/integration-mixed-milestones.test.ts} +6 -6
  175. package/src/resources/extensions/gsd/tests/{integration-proof.test.ts → integration/integration-proof.test.ts} +12 -12
  176. package/src/resources/extensions/gsd/tests/{migrate-command.test.ts → integration/migrate-command.test.ts} +2 -2
  177. package/src/resources/extensions/gsd/tests/{milestone-transition-worktree.test.ts → integration/milestone-transition-worktree.test.ts} +3 -3
  178. package/src/resources/extensions/gsd/tests/{parallel-merge.test.ts → integration/parallel-merge.test.ts} +3 -3
  179. package/src/resources/extensions/gsd/tests/{parallel-workers-multi-milestone-e2e.test.ts → integration/parallel-workers-multi-milestone-e2e.test.ts} +3 -3
  180. package/src/resources/extensions/gsd/tests/{paths.test.ts → integration/paths.test.ts} +1 -1
  181. package/src/resources/extensions/gsd/tests/{plugin-importer-live.test.ts → integration/plugin-importer-live.test.ts} +2 -2
  182. package/src/resources/extensions/gsd/tests/{queue-completed-milestone-perf.test.ts → integration/queue-completed-milestone-perf.test.ts} +3 -3
  183. package/src/resources/extensions/gsd/tests/{queue-reorder-e2e.test.ts → integration/queue-reorder-e2e.test.ts} +5 -5
  184. package/src/resources/extensions/gsd/tests/{quick-branch-lifecycle.test.ts → integration/quick-branch-lifecycle.test.ts} +5 -5
  185. package/src/resources/extensions/gsd/tests/{run-uat.test.ts → integration/run-uat.test.ts} +4 -4
  186. package/src/resources/extensions/gsd/tests/{token-savings.test.ts → integration/token-savings.test.ts} +3 -3
  187. package/src/resources/extensions/gsd/tests/{worktree-e2e.test.ts → integration/worktree-e2e.test.ts} +4 -4
  188. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +55 -0
  189. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +60 -0
  190. package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +226 -0
  191. package/src/resources/extensions/gsd/tests/plan-milestone-queue-context.test.ts +48 -0
  192. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +61 -19
  193. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +98 -0
  194. package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +59 -0
  195. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +49 -24
  196. package/src/resources/extensions/gsd/visualizer-overlay.ts +3 -3
  197. package/src/resources/extensions/shared/format-utils.ts +1 -1
  198. package/src/resources/extensions/subagent/worker-registry.ts +2 -1
  199. package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +0 -9
  200. /package/dist/web/standalone/.next/static/{nISuDzAIpGYC-DVTvs4Po → k92jvAf8IfV4dZE3nnrAr}/_buildManifest.js +0 -0
  201. /package/dist/web/standalone/.next/static/{nISuDzAIpGYC-DVTvs4Po → k92jvAf8IfV4dZE3nnrAr}/_ssgManifest.js +0 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * parallel-worker-lock-contention.test.ts — Regression tests for #2184.
3
+ *
4
+ * Covers all four bugs from the parallel worker contention issue:
5
+ * Bug 1: Session lock contention — per-milestone lock isolation
6
+ * Bug 2: Budget ceiling scoped to current session for parallel workers
7
+ * Bug 3: syncProjectRootToWorktree skips when source === destination (symlinks)
8
+ * Bug 4: createMilestoneWorktree copies planning artifacts
9
+ *
10
+ * Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
11
+ */
12
+
13
+ import {
14
+ mkdtempSync,
15
+ mkdirSync,
16
+ writeFileSync,
17
+ rmSync,
18
+ existsSync,
19
+ symlinkSync,
20
+ readFileSync,
21
+ } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { tmpdir } from "node:os";
24
+
25
+ import {
26
+ acquireSessionLock,
27
+ releaseSessionLock,
28
+ effectiveLockFile,
29
+ effectiveLockTarget,
30
+ } from "../session-lock.ts";
31
+ import { gsdRoot } from "../paths.ts";
32
+ import {
33
+ syncProjectRootToWorktree,
34
+ syncStateToProjectRoot,
35
+ } from "../auto-worktree.ts";
36
+ import { writeLock, readCrashLock, clearLock } from "../crash-recovery.ts";
37
+ import { describe, test, beforeEach, afterEach } from "node:test";
38
+ import assert from "node:assert/strict";
39
+
40
+ // ─── Bug 1: Per-milestone lock isolation ──────────────────────────────────────
41
+
42
+ describe("parallel-worker-lock-contention (#2184)", () => {
43
+ // Save and restore env vars between tests
44
+ const savedEnv: Record<string, string | undefined> = {};
45
+
46
+ beforeEach(() => {
47
+ savedEnv.GSD_PARALLEL_WORKER = process.env.GSD_PARALLEL_WORKER;
48
+ savedEnv.GSD_MILESTONE_LOCK = process.env.GSD_MILESTONE_LOCK;
49
+ });
50
+
51
+ afterEach(() => {
52
+ if (savedEnv.GSD_PARALLEL_WORKER === undefined) {
53
+ delete process.env.GSD_PARALLEL_WORKER;
54
+ } else {
55
+ process.env.GSD_PARALLEL_WORKER = savedEnv.GSD_PARALLEL_WORKER;
56
+ }
57
+ if (savedEnv.GSD_MILESTONE_LOCK === undefined) {
58
+ delete process.env.GSD_MILESTONE_LOCK;
59
+ } else {
60
+ process.env.GSD_MILESTONE_LOCK = savedEnv.GSD_MILESTONE_LOCK;
61
+ }
62
+ });
63
+
64
+ // ─── Bug 1a: effectiveLockFile returns per-milestone name ────────────────
65
+ test("Bug 1a: effectiveLockFile returns auto.lock without parallel env", () => {
66
+ delete process.env.GSD_PARALLEL_WORKER;
67
+ delete process.env.GSD_MILESTONE_LOCK;
68
+ assert.equal(effectiveLockFile(), "auto.lock");
69
+ });
70
+
71
+ test("Bug 1a: effectiveLockFile returns auto-<MID>.lock in parallel mode", () => {
72
+ process.env.GSD_PARALLEL_WORKER = "1";
73
+ process.env.GSD_MILESTONE_LOCK = "M003";
74
+ assert.equal(effectiveLockFile(), "auto-M003.lock");
75
+ });
76
+
77
+ // ─── Bug 1b: effectiveLockTarget returns per-milestone directory ─────────
78
+ test("Bug 1b: effectiveLockTarget returns gsdDir without parallel env", () => {
79
+ delete process.env.GSD_PARALLEL_WORKER;
80
+ const gsdDir = "/tmp/test/.gsd";
81
+ assert.equal(effectiveLockTarget(gsdDir), gsdDir);
82
+ });
83
+
84
+ test("Bug 1b: effectiveLockTarget returns parallel/<MID> in parallel mode", () => {
85
+ process.env.GSD_PARALLEL_WORKER = "1";
86
+ process.env.GSD_MILESTONE_LOCK = "M003";
87
+ const gsdDir = "/tmp/test/.gsd";
88
+ assert.equal(effectiveLockTarget(gsdDir), join(gsdDir, "parallel", "M003"));
89
+ });
90
+
91
+ // ─── Bug 1c: Two parallel workers acquire independent locks ──────────────
92
+ test("Bug 1c: parallel workers use per-milestone lock files, not shared auto.lock", () => {
93
+ const base = mkdtempSync(join(tmpdir(), "gsd-parallel-lock-"));
94
+ mkdirSync(join(base, ".gsd"), { recursive: true });
95
+
96
+ try {
97
+ // Simulate worker for M001
98
+ process.env.GSD_PARALLEL_WORKER = "1";
99
+ process.env.GSD_MILESTONE_LOCK = "M001";
100
+
101
+ const r1 = acquireSessionLock(base);
102
+ assert.ok(r1.acquired, "M001 worker acquires lock");
103
+
104
+ // Verify the lock file is per-milestone
105
+ const gsdDir = gsdRoot(base);
106
+ const m001LockFile = join(gsdDir, "auto-M001.lock");
107
+ assert.ok(existsSync(m001LockFile), "auto-M001.lock exists");
108
+
109
+ // The shared auto.lock should NOT exist
110
+ const sharedLockFile = join(gsdDir, "auto.lock");
111
+ assert.ok(!existsSync(sharedLockFile), "shared auto.lock does NOT exist");
112
+
113
+ // The per-milestone lock target directory should exist
114
+ const m001LockTarget = join(gsdDir, "parallel", "M001");
115
+ assert.ok(existsSync(m001LockTarget), "parallel/M001 directory exists");
116
+
117
+ releaseSessionLock(base);
118
+
119
+ // After release, per-milestone lock file should be cleaned
120
+ assert.ok(!existsSync(m001LockFile), "auto-M001.lock cleaned after release");
121
+ } finally {
122
+ delete process.env.GSD_PARALLEL_WORKER;
123
+ delete process.env.GSD_MILESTONE_LOCK;
124
+ rmSync(base, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ // ─── Bug 1d: crash-recovery uses per-milestone lock file ─────────────────
129
+ test("Bug 1d: crash-recovery writeLock/readCrashLock uses per-milestone lock in parallel mode", () => {
130
+ const base = mkdtempSync(join(tmpdir(), "gsd-parallel-crash-"));
131
+ mkdirSync(join(base, ".gsd"), { recursive: true });
132
+
133
+ try {
134
+ process.env.GSD_PARALLEL_WORKER = "1";
135
+ process.env.GSD_MILESTONE_LOCK = "M002";
136
+
137
+ writeLock(base, "execute-task", "M002/S01/T01");
138
+
139
+ const gsdDir = gsdRoot(base);
140
+ const lockFile = join(gsdDir, "auto-M002.lock");
141
+ assert.ok(existsSync(lockFile), "crash-recovery writes auto-M002.lock");
142
+
143
+ const data = readCrashLock(base);
144
+ assert.ok(data !== null, "readCrashLock reads per-milestone lock");
145
+ assert.equal(data!.unitId, "M002/S01/T01");
146
+
147
+ clearLock(base);
148
+ assert.ok(!existsSync(lockFile), "clearLock removes per-milestone lock");
149
+ } finally {
150
+ delete process.env.GSD_PARALLEL_WORKER;
151
+ delete process.env.GSD_MILESTONE_LOCK;
152
+ rmSync(base, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ // ─── Bug 3: syncProjectRootToWorktree skips same-path symlinks ───────────
157
+ test("Bug 3: syncProjectRootToWorktree skips when .gsd resolves to same path (symlink)", () => {
158
+ const base = mkdtempSync(join(tmpdir(), "gsd-symlink-sync-"));
159
+ const externalGsd = join(base, "external-gsd");
160
+ const projectRoot = join(base, "project");
161
+ const worktreePath = join(base, "worktree");
162
+
163
+ mkdirSync(externalGsd, { recursive: true });
164
+ mkdirSync(projectRoot, { recursive: true });
165
+ mkdirSync(worktreePath, { recursive: true });
166
+
167
+ // Create the external state directory with a milestone
168
+ mkdirSync(join(externalGsd, "milestones", "M001"), { recursive: true });
169
+ writeFileSync(
170
+ join(externalGsd, "milestones", "M001", "M001-ROADMAP.md"),
171
+ "# Roadmap",
172
+ );
173
+
174
+ // Symlink both project and worktree .gsd to the same external directory
175
+ symlinkSync(externalGsd, join(projectRoot, ".gsd"));
176
+ symlinkSync(externalGsd, join(worktreePath, ".gsd"));
177
+
178
+ try {
179
+ // This should NOT throw ERR_FS_CP_EINVAL — it should skip silently
180
+ let threw = false;
181
+ try {
182
+ syncProjectRootToWorktree(projectRoot, worktreePath, "M001");
183
+ } catch {
184
+ threw = true;
185
+ }
186
+ assert.ok(!threw, "syncProjectRootToWorktree does not throw on same-path symlink");
187
+
188
+ // Same for reverse direction
189
+ threw = false;
190
+ try {
191
+ syncStateToProjectRoot(worktreePath, projectRoot, "M001");
192
+ } catch {
193
+ threw = true;
194
+ }
195
+ assert.ok(!threw, "syncStateToProjectRoot does not throw on same-path symlink");
196
+ } finally {
197
+ rmSync(base, { recursive: true, force: true });
198
+ }
199
+ });
200
+
201
+ // ─── Bug 3b: sync still works when paths are different ───────────────────
202
+ test("Bug 3b: syncProjectRootToWorktree copies when .gsd paths are different", () => {
203
+ const base = mkdtempSync(join(tmpdir(), "gsd-diff-sync-"));
204
+ const projectRoot = join(base, "project");
205
+ const worktreePath = join(base, "worktree");
206
+
207
+ mkdirSync(join(projectRoot, ".gsd", "milestones", "M001"), { recursive: true });
208
+ mkdirSync(join(worktreePath, ".gsd", "milestones"), { recursive: true });
209
+
210
+ writeFileSync(
211
+ join(projectRoot, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
212
+ "# Roadmap content",
213
+ );
214
+
215
+ try {
216
+ syncProjectRootToWorktree(projectRoot, worktreePath, "M001");
217
+
218
+ // The roadmap should have been copied
219
+ const copied = join(worktreePath, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
220
+ assert.ok(existsSync(copied), "milestone roadmap copied to worktree");
221
+ assert.equal(readFileSync(copied, "utf-8"), "# Roadmap content");
222
+ } finally {
223
+ rmSync(base, { recursive: true, force: true });
224
+ }
225
+ });
226
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { buildPlanMilestonePrompt } from "../auto-prompts.ts";
8
+
9
+ function createBase(): string {
10
+ const base = mkdtempSync(join(tmpdir(), "gsd-plan-queue-"));
11
+ mkdirSync(join(base, ".gsd", "milestones", "M010"), { recursive: true });
12
+ return base;
13
+ }
14
+
15
+ function cleanup(base: string): void {
16
+ rmSync(base, { recursive: true, force: true });
17
+ }
18
+
19
+ describe("plan-milestone queue context", () => {
20
+ test("includes queue brief when planning milestone without roadmap context", async () => {
21
+ const base = createBase();
22
+ try {
23
+ writeFileSync(
24
+ join(base, ".gsd", "QUEUE.md"),
25
+ [
26
+ "# Queue",
27
+ "",
28
+ "### M010: Analytics Dashboard — Interactivity, Intelligence & Demo Readiness",
29
+ "**Vision:** Ship a polished analytics dashboard with drilldowns and AI assistance.",
30
+ "",
31
+ "## Scope",
32
+ "- Interactivity",
33
+ "- Intelligence",
34
+ "- Demo readiness",
35
+ "",
36
+ ].join("\n"),
37
+ );
38
+
39
+ const prompt = await buildPlanMilestonePrompt("M010", "M010", base);
40
+
41
+ assert.match(prompt, /Source: `\.gsd\/QUEUE\.md`/);
42
+ assert.match(prompt, /Analytics Dashboard — Interactivity, Intelligence & Demo Readiness/);
43
+ assert.match(prompt, /Ship a polished analytics dashboard/);
44
+ } finally {
45
+ cleanup(base);
46
+ }
47
+ });
48
+ });
@@ -1,17 +1,19 @@
1
1
  /**
2
- * Regression tests for #2684: preferences.md must be included in both
3
- * ROOT_STATE_FILES (sync) and copyPlanningArtifacts (initial seed).
2
+ * Regression tests for #2684 plus uppercase-preference normalization:
3
+ * preferences files are handled explicitly
4
+ * outside ROOT_STATE_FILES and prefer canonical PREFERENCES.md over the
5
+ * legacy lowercase fallback.
4
6
  *
5
7
  * Without this, post_unit_hooks and all preference-driven config silently
6
8
  * stop working inside auto-mode worktrees.
7
9
  */
8
10
  import { test } from "node:test";
9
11
  import assert from "node:assert/strict";
10
- import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
12
+ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
11
13
  import { join } from "node:path";
12
14
  import { tmpdir } from "node:os";
13
15
 
14
- test("#2684: preferences.md is NOT in ROOT_STATE_FILES (forward-only sync)", () => {
16
+ test("#2684: preferences files are NOT in ROOT_STATE_FILES (forward-only sync)", () => {
15
17
  const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
16
18
  const src = readFileSync(srcPath, "utf-8");
17
19
 
@@ -22,21 +24,23 @@ test("#2684: preferences.md is NOT in ROOT_STATE_FILES (forward-only sync)", ()
22
24
  const arrayEnd = src.indexOf("] as const", arrayStart);
23
25
  const block = src.slice(arrayStart, arrayEnd);
24
26
 
25
- // preferences.md must NOT be in ROOT_STATE_FILES — it is handled separately
27
+ // Project preferences must NOT be in ROOT_STATE_FILES — they are handled separately
26
28
  // in syncGsdStateToWorktree() (forward-only, additive). Including it in
27
29
  // ROOT_STATE_FILES would cause syncWorktreeStateBack() to overwrite the
28
30
  // authoritative project root copy (#2684).
29
31
  const entries = block.split("\n")
30
32
  .map(l => l.trim())
31
33
  .filter(l => l.startsWith('"') && l.includes(".md"));
32
- const hasPrefs = entries.some(l => l.includes("preferences.md"));
34
+ const hasPrefs = entries.some(
35
+ l => l.includes("PREFERENCES.md") || l.includes("preferences.md"),
36
+ );
33
37
  assert.ok(
34
38
  !hasPrefs,
35
- "preferences.md must NOT be in ROOT_STATE_FILES (back-sync would overwrite root)",
39
+ "preferences files must NOT be in ROOT_STATE_FILES (back-sync would overwrite root)",
36
40
  );
37
41
  });
38
42
 
39
- test("#2684: copyPlanningArtifacts file list includes preferences.md", () => {
43
+ test("copyPlanningArtifacts prefers canonical PREFERENCES.md with lowercase fallback", () => {
40
44
  const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
41
45
  const src = readFileSync(srcPath, "utf-8");
42
46
 
@@ -45,15 +49,15 @@ test("#2684: copyPlanningArtifacts file list includes preferences.md", () => {
45
49
  assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
46
50
 
47
51
  // Extract function body (up to the next top-level function)
48
- const fnBody = src.slice(fnIdx, fnIdx + 1500);
52
+ const fnBody = src.slice(fnIdx, fnIdx + 2200);
49
53
 
50
54
  assert.ok(
51
- fnBody.includes('"preferences.md"'),
52
- "preferences.md should be in copyPlanningArtifacts file list",
55
+ fnBody.includes("PROJECT_PREFERENCES_FILE") && fnBody.includes("LEGACY_PROJECT_PREFERENCES_FILE"),
56
+ "copyPlanningArtifacts should prefer canonical PREFERENCES.md and retain lowercase fallback via the shared constants",
53
57
  );
54
58
  });
55
59
 
56
- test("#2684: syncGsdStateToWorktree copies preferences.md", async () => {
60
+ test("syncGsdStateToWorktree copies canonical PREFERENCES.md", async () => {
57
61
  // Functional test: create a mock source and destination, call the sync
58
62
  const srcBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-src-"));
59
63
  const dstBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-dst-"));
@@ -63,9 +67,9 @@ test("#2684: syncGsdStateToWorktree copies preferences.md", async () => {
63
67
  mkdirSync(dstGsd, { recursive: true });
64
68
 
65
69
  try {
66
- // Write a preferences.md in source
70
+ // Write a canonical PREFERENCES.md in source
67
71
  writeFileSync(
68
- join(srcGsd, "preferences.md"),
72
+ join(srcGsd, "PREFERENCES.md"),
69
73
  "---\nversion: 1\n---\n\npost_unit_hooks:\n - name: notify\n command: echo done\n",
70
74
  );
71
75
 
@@ -73,16 +77,54 @@ test("#2684: syncGsdStateToWorktree copies preferences.md", async () => {
73
77
  const { syncGsdStateToWorktree } = await import("../auto-worktree.ts");
74
78
  syncGsdStateToWorktree(srcBase, dstBase);
75
79
 
76
- // Verify preferences.md was copied
80
+ // Verify PREFERENCES.md was copied
77
81
  assert.ok(
78
- existsSync(join(dstGsd, "preferences.md")),
79
- "preferences.md should be copied to worktree",
82
+ existsSync(join(dstGsd, "PREFERENCES.md")),
83
+ "PREFERENCES.md should be copied to worktree",
80
84
  );
81
85
 
82
- const content = readFileSync(join(dstGsd, "preferences.md"), "utf-8");
86
+ const content = readFileSync(join(dstGsd, "PREFERENCES.md"), "utf-8");
83
87
  assert.ok(
84
88
  content.includes("post_unit_hooks"),
85
- "copied preferences.md should contain the hooks config",
89
+ "copied PREFERENCES.md should contain the hooks config",
90
+ );
91
+ } finally {
92
+ rmSync(srcBase, { recursive: true, force: true });
93
+ rmSync(dstBase, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("syncGsdStateToWorktree falls back to legacy lowercase preferences.md", async () => {
98
+ const srcBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-legacy-src-"));
99
+ const dstBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-legacy-dst-"));
100
+ const srcGsd = join(srcBase, ".gsd");
101
+ const dstGsd = join(dstBase, ".gsd");
102
+ mkdirSync(srcGsd, { recursive: true });
103
+ mkdirSync(dstGsd, { recursive: true });
104
+
105
+ try {
106
+ writeFileSync(
107
+ join(srcGsd, "preferences.md"),
108
+ "---\nversion: 1\n---\n\ngit:\n auto_push: true\n",
109
+ );
110
+
111
+ const { syncGsdStateToWorktree } = await import("../auto-worktree.ts");
112
+ const result = syncGsdStateToWorktree(srcBase, dstBase);
113
+
114
+ const copiedEntries = readdirSync(dstGsd)
115
+ .filter((name) => name === "PREFERENCES.md" || name === "preferences.md");
116
+
117
+ assert.ok(
118
+ copiedEntries.length === 1,
119
+ `expected exactly one preferences file in worktree, got ${copiedEntries.join(", ") || "(none)"}`,
120
+ );
121
+ assert.ok(
122
+ copiedEntries[0] === "PREFERENCES.md" || copiedEntries[0] === "preferences.md",
123
+ "legacy fallback should still result in one readable preferences file",
124
+ );
125
+ assert.ok(
126
+ result.synced.includes("preferences.md") || result.synced.includes("PREFERENCES.md"),
127
+ "legacy fallback copy should be reported in synced list",
86
128
  );
87
129
  } finally {
88
130
  rmSync(srcBase, { recursive: true, force: true });
@@ -12,6 +12,7 @@ import { join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { classifyError, isTransient, isTransientNetworkError } from "../error-classifier.ts";
14
14
  import { pauseAutoForProviderError } from "../provider-error-pause.ts";
15
+ import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.ts";
15
16
  import { getNextFallbackModel } from "../preferences.ts";
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -268,6 +269,90 @@ test("pauseAutoForProviderError falls back to indefinite pause when not rate lim
268
269
  ]);
269
270
  });
270
271
 
272
+ // ── resumeAutoAfterProviderDelay ────────────────────────────────────────────
273
+
274
+ test("resumeAutoAfterProviderDelay restarts paused auto-mode from the recorded base path", async () => {
275
+ const startCalls: Array<{ base: string; verboseMode: boolean; step?: boolean }> = [];
276
+ const result = await resumeAutoAfterProviderDelay(
277
+ {} as any,
278
+ { ui: { notify() {} } } as any,
279
+ {
280
+ getSnapshot: () => ({
281
+ active: false,
282
+ paused: true,
283
+ stepMode: true,
284
+ basePath: "/tmp/project",
285
+ }),
286
+ startAuto: async (_ctx, _pi, base, verboseMode, options) => {
287
+ startCalls.push({ base, verboseMode, step: options?.step });
288
+ },
289
+ },
290
+ );
291
+
292
+ assert.equal(result, "resumed");
293
+ assert.deepEqual(startCalls, [
294
+ { base: "/tmp/project", verboseMode: false, step: true },
295
+ ]);
296
+ });
297
+
298
+ test("resumeAutoAfterProviderDelay does not double-start when auto-mode is already active", async () => {
299
+ let startCalls = 0;
300
+ const result = await resumeAutoAfterProviderDelay(
301
+ {} as any,
302
+ { ui: { notify() {} } } as any,
303
+ {
304
+ getSnapshot: () => ({
305
+ active: true,
306
+ paused: false,
307
+ stepMode: false,
308
+ basePath: "/tmp/project",
309
+ }),
310
+ startAuto: async () => {
311
+ startCalls += 1;
312
+ },
313
+ },
314
+ );
315
+
316
+ assert.equal(result, "already-active");
317
+ assert.equal(startCalls, 0);
318
+ });
319
+
320
+ test("resumeAutoAfterProviderDelay leaves auto paused when no base path is available", async () => {
321
+ const notifications: Array<{ message: string; level: string }> = [];
322
+ let startCalls = 0;
323
+
324
+ const result = await resumeAutoAfterProviderDelay(
325
+ {} as any,
326
+ {
327
+ ui: {
328
+ notify(message: string, level?: string) {
329
+ notifications.push({ message, level: level ?? "info" });
330
+ },
331
+ },
332
+ } as any,
333
+ {
334
+ getSnapshot: () => ({
335
+ active: false,
336
+ paused: true,
337
+ stepMode: false,
338
+ basePath: "",
339
+ }),
340
+ startAuto: async () => {
341
+ startCalls += 1;
342
+ },
343
+ },
344
+ );
345
+
346
+ assert.equal(result, "missing-base");
347
+ assert.equal(startCalls, 0);
348
+ assert.deepEqual(notifications, [
349
+ {
350
+ message: "Provider error recovery delay elapsed, but no paused auto-mode base path was available. Leaving auto-mode paused.",
351
+ level: "warning",
352
+ },
353
+ ]);
354
+ });
355
+
271
356
  // ── Escalating backoff for transient errors (#1166) ─────────────────────────
272
357
 
273
358
  test("agent-end-recovery.ts tracks consecutive transient errors for escalating backoff", () => {
@@ -303,6 +388,19 @@ test("agent-end-recovery.ts applies escalating delay for repeated transient erro
303
388
  );
304
389
  });
305
390
 
391
+ test("agent-end-recovery.ts resumes transient provider pauses through startAuto instead of a hidden prompt", () => {
392
+ const src = readFileSync(join(__dirname, "..", "bootstrap", "agent-end-recovery.ts"), "utf-8");
393
+
394
+ assert.ok(
395
+ src.includes("resumeAutoAfterProviderDelay"),
396
+ "agent-end-recovery.ts must resume paused auto-mode through resumeAutoAfterProviderDelay (#2813)",
397
+ );
398
+ assert.ok(
399
+ !src.includes('Continue execution — provider error recovery delay elapsed.'),
400
+ "transient provider resume must not rely on a hidden continue prompt (#2813)",
401
+ );
402
+ });
403
+
306
404
  // ── Codex error extraction (#1166) ──────────────────────────────────────────
307
405
 
308
406
  test("openai-codex-responses.ts extracts nested error fields", () => {
@@ -0,0 +1,59 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { handleRecoverableExtensionProcessError } from "../bootstrap/register-extension.ts";
5
+
6
+ test("handleRecoverableExtensionProcessError swallows spawn ENOENT", () => {
7
+ let stderr = "";
8
+ const originalWrite = process.stderr.write.bind(process.stderr);
9
+ process.stderr.write = ((chunk: string | Uint8Array) => {
10
+ stderr += String(chunk);
11
+ return true;
12
+ }) as typeof process.stderr.write;
13
+
14
+ try {
15
+ const handled = handleRecoverableExtensionProcessError(
16
+ Object.assign(new Error("missing binary"), {
17
+ code: "ENOENT",
18
+ syscall: "spawn npm",
19
+ path: "npm",
20
+ }),
21
+ );
22
+ assert.equal(handled, true);
23
+ assert.match(stderr, /spawn ENOENT: npm/);
24
+ } finally {
25
+ process.stderr.write = originalWrite;
26
+ }
27
+ });
28
+
29
+ test("handleRecoverableExtensionProcessError swallows uv_cwd ENOENT", () => {
30
+ let stderr = "";
31
+ const originalWrite = process.stderr.write.bind(process.stderr);
32
+ process.stderr.write = ((chunk: string | Uint8Array) => {
33
+ stderr += String(chunk);
34
+ return true;
35
+ }) as typeof process.stderr.write;
36
+
37
+ try {
38
+ const handled = handleRecoverableExtensionProcessError(
39
+ Object.assign(new Error("process.cwd failed"), {
40
+ code: "ENOENT",
41
+ syscall: "uv_cwd",
42
+ }),
43
+ );
44
+ assert.equal(handled, true);
45
+ assert.match(stderr, /ENOENT \(uv_cwd\): process\.cwd failed/);
46
+ } finally {
47
+ process.stderr.write = originalWrite;
48
+ }
49
+ });
50
+
51
+ test("handleRecoverableExtensionProcessError leaves unrelated errors unhandled", () => {
52
+ const handled = handleRecoverableExtensionProcessError(
53
+ Object.assign(new Error("permission denied"), {
54
+ code: "EPERM",
55
+ syscall: "open",
56
+ }),
57
+ );
58
+ assert.equal(handled, false);
59
+ });