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.
- package/dist/cli.js +19 -19
- package/dist/headless-ui.d.ts +27 -1
- package/dist/headless-ui.js +203 -13
- package/dist/headless.js +60 -3
- package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +2 -2
- package/dist/resources/extensions/bg-shell/utilities.js +34 -5
- package/dist/resources/extensions/gsd/auto/phases.js +19 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +17 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
- package/dist/resources/extensions/gsd/auto-start.js +12 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +39 -14
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +5 -1
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +18 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -5
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +20 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +15 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -2
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +413 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -1
- package/dist/resources/extensions/gsd/session-lock.js +46 -12
- package/dist/resources/extensions/gsd/skill-health.js +2 -2
- package/dist/resources/extensions/gsd/visualizer-overlay.js +3 -3
- package/dist/resources/extensions/shared/format-utils.js +1 -1
- package/dist/resources/extensions/subagent/worker-registry.js +2 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/6502.2305d0afd2385711.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/{page-b950e4e384cc62b3.js → page-0c485498795110d6.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/{webpack-bca0e732db0dcec3.js → webpack-4332cbd5dd1be584.js} +1 -1
- package/package.json +6 -4
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +14 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-registry.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -2
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +45 -41
- package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +2 -2
- package/src/resources/extensions/bg-shell/utilities.ts +39 -4
- package/src/resources/extensions/gsd/auto/phases.ts +25 -4
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +21 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +13 -5
- package/src/resources/extensions/gsd/auto-worktree.ts +46 -13
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +53 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -6
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +24 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +19 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +2 -3
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +497 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +6 -1
- package/src/resources/extensions/gsd/session-lock.ts +46 -12
- package/src/resources/extensions/gsd/skill-health.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/{all-milestones-complete-merge.test.ts → integration/all-milestones-complete-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{atomic-task-closeout.test.ts → integration/atomic-task-closeout.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{auto-preflight.test.ts → integration/auto-preflight.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{auto-recovery.test.ts → integration/auto-recovery.test.ts} +7 -7
- package/src/resources/extensions/gsd/tests/{auto-secrets-gate.test.ts → integration/auto-secrets-gate.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{auto-stash-merge.test.ts → integration/auto-stash-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{auto-worktree-milestone-merge.test.ts → integration/auto-worktree-milestone-merge.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{auto-worktree.test.ts → integration/auto-worktree.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{continue-here.test.ts → integration/continue-here.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{doctor-completion-deferral.test.ts → integration/doctor-completion-deferral.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-delimiter-fix.test.ts → integration/doctor-delimiter-fix.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-enhancements.test.ts → integration/doctor-enhancements.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{doctor-environment-worktree.test.ts → integration/doctor-environment-worktree.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-environment.test.ts → integration/doctor-environment.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-fixlevel.test.ts → integration/doctor-fixlevel.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{doctor-git.test.ts → integration/doctor-git.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-proactive.test.ts → integration/doctor-proactive.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-roadmap-summary-atomicity.test.ts → integration/doctor-roadmap-summary-atomicity.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-runtime.test.ts → integration/doctor-runtime.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor.test.ts → integration/doctor.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{e2e-workflow-pipeline-integration.test.ts → integration/e2e-workflow-pipeline-integration.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{feature-branch-lifecycle-integration.test.ts → integration/feature-branch-lifecycle-integration.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{git-locale.test.ts → integration/git-locale.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{git-self-heal.test.ts → integration/git-self-heal.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{git-service.test.ts → integration/git-service.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{gitignore-tracked-gsd.test.ts → integration/gitignore-tracked-gsd.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{idle-recovery.test.ts → integration/idle-recovery.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{inherited-repo-home-dir.test.ts → integration/inherited-repo-home-dir.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{integration-lifecycle.test.ts → integration/integration-lifecycle.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{integration-mixed-milestones.test.ts → integration/integration-mixed-milestones.test.ts} +6 -6
- package/src/resources/extensions/gsd/tests/{integration-proof.test.ts → integration/integration-proof.test.ts} +12 -12
- package/src/resources/extensions/gsd/tests/{migrate-command.test.ts → integration/migrate-command.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{milestone-transition-worktree.test.ts → integration/milestone-transition-worktree.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{parallel-merge.test.ts → integration/parallel-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{parallel-workers-multi-milestone-e2e.test.ts → integration/parallel-workers-multi-milestone-e2e.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{paths.test.ts → integration/paths.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{plugin-importer-live.test.ts → integration/plugin-importer-live.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{queue-completed-milestone-perf.test.ts → integration/queue-completed-milestone-perf.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{queue-reorder-e2e.test.ts → integration/queue-reorder-e2e.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{quick-branch-lifecycle.test.ts → integration/quick-branch-lifecycle.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{run-uat.test.ts → integration/run-uat.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{token-savings.test.ts → integration/token-savings.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{worktree-e2e.test.ts → integration/worktree-e2e.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-queue-context.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +61 -19
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +49 -24
- package/src/resources/extensions/gsd/visualizer-overlay.ts +3 -3
- package/src/resources/extensions/shared/format-utils.ts +1 -1
- package/src/resources/extensions/subagent/worker-registry.ts +2 -1
- package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +0 -9
- /package/dist/web/standalone/.next/static/{nISuDzAIpGYC-DVTvs4Po → k92jvAf8IfV4dZE3nnrAr}/_buildManifest.js +0 -0
- /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
|
|
3
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
39
|
+
"preferences files must NOT be in ROOT_STATE_FILES (back-sync would overwrite root)",
|
|
36
40
|
);
|
|
37
41
|
});
|
|
38
42
|
|
|
39
|
-
test("
|
|
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 +
|
|
52
|
+
const fnBody = src.slice(fnIdx, fnIdx + 2200);
|
|
49
53
|
|
|
50
54
|
assert.ok(
|
|
51
|
-
fnBody.includes(
|
|
52
|
-
"
|
|
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("
|
|
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
|
|
70
|
+
// Write a canonical PREFERENCES.md in source
|
|
67
71
|
writeFileSync(
|
|
68
|
-
join(srcGsd, "
|
|
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
|
|
80
|
+
// Verify PREFERENCES.md was copied
|
|
77
81
|
assert.ok(
|
|
78
|
-
existsSync(join(dstGsd, "
|
|
79
|
-
"
|
|
82
|
+
existsSync(join(dstGsd, "PREFERENCES.md")),
|
|
83
|
+
"PREFERENCES.md should be copied to worktree",
|
|
80
84
|
);
|
|
81
85
|
|
|
82
|
-
const content = readFileSync(join(dstGsd, "
|
|
86
|
+
const content = readFileSync(join(dstGsd, "PREFERENCES.md"), "utf-8");
|
|
83
87
|
assert.ok(
|
|
84
88
|
content.includes("post_unit_hooks"),
|
|
85
|
-
"copied
|
|
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
|
+
});
|