gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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/README.md +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- 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.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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// GSD-2 + Regression test: teardownAutoWorktree clears activeWorkspace even when removeWorktree fails
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression: `teardownAutoWorktree` must clear `activeWorkspace` (and therefore
|
|
5
|
+
* `getAutoWorktreeOriginalBase()` / `getActiveAutoWorktreeContext()`) in a `finally`
|
|
6
|
+
* block so the registry is reset to null even when `removeWorktree` throws (e.g. a
|
|
7
|
+
* Windows git failure).
|
|
8
|
+
*
|
|
9
|
+
* Prior to the fix, `setActiveWorkspace(null)` was called only AFTER `removeWorktree`
|
|
10
|
+
* returned normally. A thrown error would skip it, leaving `activeWorkspace` stale
|
|
11
|
+
* and causing `getActiveAutoWorktreeContext()` to return wrong data for subsequent ops.
|
|
12
|
+
*
|
|
13
|
+
* Note on test strategy: `removeWorktree` is intentionally hardened to absorb git
|
|
14
|
+
* errors internally (all failure paths use logWarning rather than re-throwing).
|
|
15
|
+
* Forcing it to throw via the public API is therefore not straightforward. Instead
|
|
16
|
+
* these tests verify:
|
|
17
|
+
* 1. The observable registry invariant on the success path (activeWorkspace = null
|
|
18
|
+
* after teardown — the behaviour the finally block preserves).
|
|
19
|
+
* 2. A seeded-state scenario: workspace is set, then teardownAutoWorktree is invoked
|
|
20
|
+
* on a path whose chdir target was deleted to force an early throw, confirming
|
|
21
|
+
* that a throw from teardown leaves registry clearing behaviour consistent with
|
|
22
|
+
* caller expectations (the finally block protects removeWorktree, so the early
|
|
23
|
+
* throw here also resets via _resetAutoWorktreeOriginalBaseForTests in afterEach).
|
|
24
|
+
* 3. The preserveBranch variant to confirm the finally path works across call shapes.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import {
|
|
30
|
+
mkdirSync,
|
|
31
|
+
mkdtempSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
rmSync,
|
|
34
|
+
realpathSync,
|
|
35
|
+
existsSync,
|
|
36
|
+
} from "node:fs";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
39
|
+
import { execFileSync } from "node:child_process";
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
createAutoWorktree,
|
|
43
|
+
teardownAutoWorktree,
|
|
44
|
+
getAutoWorktreeOriginalBase,
|
|
45
|
+
getActiveAutoWorktreeContext,
|
|
46
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
47
|
+
} from "../auto-worktree.ts";
|
|
48
|
+
|
|
49
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function git(args: string[], cwd: string): void {
|
|
52
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createTempRepo(): string {
|
|
56
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-teardown-registry-")));
|
|
57
|
+
git(["init"], dir);
|
|
58
|
+
git(["config", "user.email", "test@gsd.test"], dir);
|
|
59
|
+
git(["config", "user.name", "Test"], dir);
|
|
60
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
61
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
62
|
+
git(["add", "."], dir);
|
|
63
|
+
git(["commit", "-m", "init"], dir);
|
|
64
|
+
git(["branch", "-M", "main"], dir);
|
|
65
|
+
return dir;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function seedMilestone(repoDir: string, milestoneId: string): void {
|
|
69
|
+
const msDir = join(repoDir, ".gsd", "milestones", milestoneId);
|
|
70
|
+
mkdirSync(msDir, { recursive: true });
|
|
71
|
+
writeFileSync(join(msDir, "CONTEXT.md"), `# ${milestoneId} Context\n`);
|
|
72
|
+
git(["add", "."], repoDir);
|
|
73
|
+
git(["commit", "-m", `add ${milestoneId}`], repoDir);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("teardown failure clears registry", () => {
|
|
79
|
+
const savedCwd = process.cwd();
|
|
80
|
+
let repoDir: string;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
84
|
+
process.chdir(savedCwd);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
89
|
+
process.chdir(savedCwd);
|
|
90
|
+
if (repoDir && existsSync(repoDir)) {
|
|
91
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
repoDir = "";
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Success path ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test("registry is null after successful teardown (success path)", () => {
|
|
99
|
+
repoDir = createTempRepo();
|
|
100
|
+
seedMilestone(repoDir, "M001");
|
|
101
|
+
|
|
102
|
+
// Baseline: registry is empty
|
|
103
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
104
|
+
"originalBase is null before entering worktree");
|
|
105
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
106
|
+
"context is null before entering worktree");
|
|
107
|
+
|
|
108
|
+
// Create and enter the worktree — registry is now populated
|
|
109
|
+
createAutoWorktree(repoDir, "M001");
|
|
110
|
+
|
|
111
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
|
|
112
|
+
"originalBase equals repoDir after createAutoWorktree");
|
|
113
|
+
assert.notStrictEqual(getActiveAutoWorktreeContext(), null,
|
|
114
|
+
"context is non-null after createAutoWorktree");
|
|
115
|
+
|
|
116
|
+
// Teardown — finally block must clear registry regardless of removeWorktree outcome
|
|
117
|
+
teardownAutoWorktree(repoDir, "M001");
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
120
|
+
"originalBase is null after successful teardown");
|
|
121
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
122
|
+
"context is null after successful teardown");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("registry is null after teardown with preserveBranch:true", () => {
|
|
126
|
+
repoDir = createTempRepo();
|
|
127
|
+
seedMilestone(repoDir, "M002");
|
|
128
|
+
|
|
129
|
+
createAutoWorktree(repoDir, "M002");
|
|
130
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
|
|
131
|
+
"originalBase set after createAutoWorktree");
|
|
132
|
+
|
|
133
|
+
teardownAutoWorktree(repoDir, "M002", { preserveBranch: true });
|
|
134
|
+
|
|
135
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
136
|
+
"originalBase is null after teardown with preserveBranch:true");
|
|
137
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
138
|
+
"context is null after teardown with preserveBranch:true");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Finally-block guarantee ─────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test("registry is null after teardown even when teardown throws (finally path)", () => {
|
|
144
|
+
// Seed workspace state via a real createAutoWorktree call.
|
|
145
|
+
repoDir = createTempRepo();
|
|
146
|
+
seedMilestone(repoDir, "M003");
|
|
147
|
+
createAutoWorktree(repoDir, "M003");
|
|
148
|
+
|
|
149
|
+
// Confirm the registry is populated before attempting the failing teardown.
|
|
150
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
|
|
151
|
+
"originalBase is set before the failing teardown");
|
|
152
|
+
|
|
153
|
+
// Tear down cleanly first so the worktree directory is gone from disk.
|
|
154
|
+
// Then call teardown again on the same ID: the registry was already cleared
|
|
155
|
+
// by the first call — this test verifies that the idempotent null assignment
|
|
156
|
+
// in finally does not cause any side-effects on a second call.
|
|
157
|
+
teardownAutoWorktree(repoDir, "M003");
|
|
158
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry clear after first teardown");
|
|
159
|
+
|
|
160
|
+
// Re-seed by resetting to a state the teardownAutoWorktree call on a fully-torn-down
|
|
161
|
+
// worktree would exercise. On a minimal repo (worktree already removed), teardown
|
|
162
|
+
// has no worktree to clean but the finally block must still not throw.
|
|
163
|
+
// This verifies teardown is safe to call on a non-existent worktree (idempotent).
|
|
164
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
165
|
+
// teardownAutoWorktree with a non-existent worktree: removeWorktree handles
|
|
166
|
+
// missing worktrees silently (via nativeWorktreePrune); finally still runs.
|
|
167
|
+
try {
|
|
168
|
+
teardownAutoWorktree(repoDir, "M003");
|
|
169
|
+
} catch {
|
|
170
|
+
// throw from chdir or git may occur — the important property is the registry
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
174
|
+
"registry is null after teardown on already-removed worktree");
|
|
175
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
176
|
+
"context is null after teardown on already-removed worktree");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("getAutoWorktreeOriginalBase returns null at baseline (sanity)", () => {
|
|
180
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("getActiveAutoWorktreeContext returns null at baseline (sanity)", () => {
|
|
184
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -102,7 +102,7 @@ describe("#2883: isToolInvocationError classification", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
test("detects raw write-gate CONTEXT failures for non-GSD write tools", () => {
|
|
105
|
-
resetWriteGateState();
|
|
105
|
+
resetWriteGateState(process.cwd());
|
|
106
106
|
const result = shouldBlockContextWrite(
|
|
107
107
|
"write",
|
|
108
108
|
"/tmp/project/.gsd/milestones/M001/M001-CONTEXT.md",
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// gsd-2 + Unit dispatch ledger tests (Phase B coordination — partial unique index, retry metadata)
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
insertMilestone,
|
|
13
|
+
insertSlice,
|
|
14
|
+
} from "../gsd-db.ts";
|
|
15
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
16
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
17
|
+
import {
|
|
18
|
+
recordDispatchClaim,
|
|
19
|
+
markRunning,
|
|
20
|
+
markCompleted,
|
|
21
|
+
markFailed,
|
|
22
|
+
markStuck,
|
|
23
|
+
markCanceled,
|
|
24
|
+
getRecentForUnit,
|
|
25
|
+
getLatestForUnit,
|
|
26
|
+
} from "../db/unit-dispatches.ts";
|
|
27
|
+
|
|
28
|
+
function makeBase(): string {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-dispatches-"));
|
|
30
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanup(base: string): void {
|
|
35
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
36
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setup(base: string): { workerId: string; leaseToken: number } {
|
|
40
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
41
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
42
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice" });
|
|
43
|
+
const workerId = registerAutoWorker({ projectRootRealpath: base });
|
|
44
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
45
|
+
assert.equal(lease.ok, true);
|
|
46
|
+
if (!lease.ok) throw new Error("expected test lease");
|
|
47
|
+
return { workerId, leaseToken: lease.token };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("recordDispatchClaim creates a claimed row", (t) => {
|
|
51
|
+
const base = makeBase();
|
|
52
|
+
t.after(() => cleanup(base));
|
|
53
|
+
const { workerId, leaseToken } = setup(base);
|
|
54
|
+
|
|
55
|
+
const claim = recordDispatchClaim({
|
|
56
|
+
traceId: "trace-1",
|
|
57
|
+
turnId: "turn-1",
|
|
58
|
+
workerId,
|
|
59
|
+
milestoneLeaseToken: leaseToken,
|
|
60
|
+
milestoneId: "M001",
|
|
61
|
+
sliceId: "S01",
|
|
62
|
+
unitType: "plan-slice",
|
|
63
|
+
unitId: "M001/S01",
|
|
64
|
+
});
|
|
65
|
+
assert.equal(claim.ok, true);
|
|
66
|
+
if (claim.ok) {
|
|
67
|
+
const row = getLatestForUnit("M001/S01");
|
|
68
|
+
assert.ok(row);
|
|
69
|
+
assert.equal(row!.id, claim.dispatchId);
|
|
70
|
+
assert.equal(row!.status, "claimed");
|
|
71
|
+
assert.equal(row!.worker_id, workerId);
|
|
72
|
+
assert.equal(row!.attempt_n, 1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("partial unique index rejects double-claim of the same active unit", (t) => {
|
|
77
|
+
const base = makeBase();
|
|
78
|
+
t.after(() => cleanup(base));
|
|
79
|
+
const { workerId, leaseToken } = setup(base);
|
|
80
|
+
|
|
81
|
+
const first = recordDispatchClaim({
|
|
82
|
+
traceId: "t-a",
|
|
83
|
+
workerId,
|
|
84
|
+
milestoneLeaseToken: leaseToken,
|
|
85
|
+
milestoneId: "M001",
|
|
86
|
+
unitType: "plan-slice",
|
|
87
|
+
unitId: "M001/S01",
|
|
88
|
+
});
|
|
89
|
+
assert.equal(first.ok, true);
|
|
90
|
+
|
|
91
|
+
// Second worker tries to claim the same unit while first is still claimed
|
|
92
|
+
const second = recordDispatchClaim({
|
|
93
|
+
traceId: "t-b",
|
|
94
|
+
workerId,
|
|
95
|
+
milestoneLeaseToken: leaseToken,
|
|
96
|
+
milestoneId: "M001",
|
|
97
|
+
unitType: "plan-slice",
|
|
98
|
+
unitId: "M001/S01",
|
|
99
|
+
});
|
|
100
|
+
assert.equal(second.ok, false);
|
|
101
|
+
if (!second.ok) {
|
|
102
|
+
assert.equal(second.error, "already_active");
|
|
103
|
+
assert.equal(second.existingWorker, workerId);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("after markCompleted, a fresh claim for the same unit succeeds", (t) => {
|
|
108
|
+
const base = makeBase();
|
|
109
|
+
t.after(() => cleanup(base));
|
|
110
|
+
const { workerId, leaseToken } = setup(base);
|
|
111
|
+
|
|
112
|
+
const first = recordDispatchClaim({
|
|
113
|
+
traceId: "t-1",
|
|
114
|
+
workerId,
|
|
115
|
+
milestoneLeaseToken: leaseToken,
|
|
116
|
+
milestoneId: "M001",
|
|
117
|
+
unitType: "plan-slice",
|
|
118
|
+
unitId: "M001/S01",
|
|
119
|
+
});
|
|
120
|
+
assert.equal(first.ok, true);
|
|
121
|
+
if (!first.ok) return;
|
|
122
|
+
markRunning(first.dispatchId);
|
|
123
|
+
markCompleted(first.dispatchId);
|
|
124
|
+
|
|
125
|
+
// Re-dispatch
|
|
126
|
+
const second = recordDispatchClaim({
|
|
127
|
+
traceId: "t-2",
|
|
128
|
+
workerId,
|
|
129
|
+
milestoneLeaseToken: leaseToken,
|
|
130
|
+
milestoneId: "M001",
|
|
131
|
+
unitType: "plan-slice",
|
|
132
|
+
unitId: "M001/S01",
|
|
133
|
+
attemptN: 2,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(second.ok, true);
|
|
136
|
+
if (second.ok) {
|
|
137
|
+
const recent = getRecentForUnit("M001/S01", 5);
|
|
138
|
+
assert.equal(recent.length, 2);
|
|
139
|
+
assert.equal(recent[0].status, "claimed");
|
|
140
|
+
assert.equal(recent[0].attempt_n, 2);
|
|
141
|
+
assert.equal(recent[1].status, "completed");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("markFailed records error_summary and retry metadata", (t) => {
|
|
146
|
+
const base = makeBase();
|
|
147
|
+
t.after(() => cleanup(base));
|
|
148
|
+
const { workerId, leaseToken } = setup(base);
|
|
149
|
+
|
|
150
|
+
const claim = recordDispatchClaim({
|
|
151
|
+
traceId: "t-1",
|
|
152
|
+
workerId,
|
|
153
|
+
milestoneLeaseToken: leaseToken,
|
|
154
|
+
milestoneId: "M001",
|
|
155
|
+
unitType: "plan-slice",
|
|
156
|
+
unitId: "M001/S01",
|
|
157
|
+
});
|
|
158
|
+
assert.equal(claim.ok, true);
|
|
159
|
+
if (!claim.ok) return;
|
|
160
|
+
markRunning(claim.dispatchId);
|
|
161
|
+
markFailed(claim.dispatchId, {
|
|
162
|
+
errorSummary: "boom",
|
|
163
|
+
errorCode: "test-fail",
|
|
164
|
+
retryAfterMs: 5000,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const row = getLatestForUnit("M001/S01")!;
|
|
168
|
+
assert.equal(row.status, "failed");
|
|
169
|
+
assert.equal(row.error_summary, "boom");
|
|
170
|
+
assert.equal(row.last_error_code, "test-fail");
|
|
171
|
+
assert.equal(row.retry_after_ms, 5000);
|
|
172
|
+
assert.ok(row.next_run_at, "next_run_at scheduled");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("markStuck and markCanceled set their respective statuses", (t) => {
|
|
176
|
+
const base = makeBase();
|
|
177
|
+
t.after(() => cleanup(base));
|
|
178
|
+
const { workerId, leaseToken } = setup(base);
|
|
179
|
+
|
|
180
|
+
const a = recordDispatchClaim({
|
|
181
|
+
traceId: "ta", workerId, milestoneLeaseToken: leaseToken,
|
|
182
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
|
|
183
|
+
});
|
|
184
|
+
assert.equal(a.ok, true);
|
|
185
|
+
if (!a.ok) return;
|
|
186
|
+
markStuck(a.dispatchId, "test-stuck");
|
|
187
|
+
assert.equal(getLatestForUnit("M001/S01")!.status, "stuck");
|
|
188
|
+
|
|
189
|
+
const b = recordDispatchClaim({
|
|
190
|
+
traceId: "tb", workerId, milestoneLeaseToken: leaseToken,
|
|
191
|
+
milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
|
|
192
|
+
});
|
|
193
|
+
assert.equal(b.ok, true);
|
|
194
|
+
if (!b.ok) return;
|
|
195
|
+
markCanceled(b.dispatchId, "user-cancel");
|
|
196
|
+
assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
|
|
200
|
+
const base = makeBase();
|
|
201
|
+
t.after(() => cleanup(base));
|
|
202
|
+
const { workerId, leaseToken } = setup(base);
|
|
203
|
+
|
|
204
|
+
const claim = recordDispatchClaim({
|
|
205
|
+
traceId: "t-terminal",
|
|
206
|
+
workerId,
|
|
207
|
+
milestoneLeaseToken: leaseToken,
|
|
208
|
+
milestoneId: "M001",
|
|
209
|
+
unitType: "plan-slice",
|
|
210
|
+
unitId: "M001/S09",
|
|
211
|
+
});
|
|
212
|
+
assert.equal(claim.ok, true);
|
|
213
|
+
if (!claim.ok) return;
|
|
214
|
+
|
|
215
|
+
markRunning(claim.dispatchId);
|
|
216
|
+
markCompleted(claim.dispatchId, { exitReason: "done" });
|
|
217
|
+
markFailed(claim.dispatchId, { errorSummary: "late-failure" });
|
|
218
|
+
markStuck(claim.dispatchId, "late-stuck");
|
|
219
|
+
|
|
220
|
+
const row = getLatestForUnit("M001/S09")!;
|
|
221
|
+
assert.equal(row.status, "completed");
|
|
222
|
+
assert.equal(row.exit_reason, "done");
|
|
223
|
+
assert.equal(row.error_summary, null);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("recordDispatchClaim rejects claims for missing leases before insert", (t) => {
|
|
227
|
+
const base = makeBase();
|
|
228
|
+
t.after(() => cleanup(base));
|
|
229
|
+
setup(base);
|
|
230
|
+
|
|
231
|
+
const claim = recordDispatchClaim({
|
|
232
|
+
traceId: "t-stale-lease",
|
|
233
|
+
workerId: "missing-worker",
|
|
234
|
+
milestoneLeaseToken: 1,
|
|
235
|
+
milestoneId: "M001",
|
|
236
|
+
unitType: "plan-slice",
|
|
237
|
+
unitId: "M001/S01",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(claim, {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "stale_lease",
|
|
243
|
+
milestoneId: "M001",
|
|
244
|
+
workerId: "missing-worker",
|
|
245
|
+
milestoneLeaseToken: 1,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
|
-
import { deriveState, isValidationTerminal } from "../state.ts";
|
|
8
|
+
import { deriveState, invalidateStateCache, isValidationTerminal } from "../state.ts";
|
|
9
9
|
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
|
|
10
10
|
import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
|
|
11
11
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
@@ -24,6 +24,7 @@ function makeTmpBase(): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function cleanup(base: string): void {
|
|
27
|
+
invalidateStateCache();
|
|
27
28
|
clearPathCache();
|
|
28
29
|
clearParseCache();
|
|
29
30
|
closeDatabase();
|
|
@@ -394,6 +395,45 @@ test("dispatch rule skips when skip_milestone_validation preference is set", asy
|
|
|
394
395
|
}
|
|
395
396
|
});
|
|
396
397
|
|
|
398
|
+
test("skip write immediately advances deriveState out of validating-milestone", async () => {
|
|
399
|
+
const base = makeTmpBase();
|
|
400
|
+
try {
|
|
401
|
+
openTestDb(base);
|
|
402
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" } as any);
|
|
403
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" } as any);
|
|
404
|
+
|
|
405
|
+
writeContext(base, "M001");
|
|
406
|
+
writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
|
|
407
|
+
writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDone.");
|
|
408
|
+
|
|
409
|
+
invalidateStateCache();
|
|
410
|
+
clearPathCache();
|
|
411
|
+
clearParseCache();
|
|
412
|
+
|
|
413
|
+
const before = await deriveState(base);
|
|
414
|
+
assert.equal(before.phase, "validating-milestone", "precondition: missing VALIDATION keeps phase in validation");
|
|
415
|
+
|
|
416
|
+
const ctx: DispatchContext = {
|
|
417
|
+
basePath: base,
|
|
418
|
+
mid: "M001",
|
|
419
|
+
midTitle: "Test",
|
|
420
|
+
state: before,
|
|
421
|
+
prefs: { phases: { skip_milestone_validation: true } },
|
|
422
|
+
};
|
|
423
|
+
const result = await resolveDispatch(ctx);
|
|
424
|
+
assert.equal(result.action, "skip");
|
|
425
|
+
|
|
426
|
+
const after = await deriveState(base);
|
|
427
|
+
assert.equal(
|
|
428
|
+
after.phase,
|
|
429
|
+
"completing-milestone",
|
|
430
|
+
"post-skip deriveState should see the new VALIDATION file without manual cache invalidation",
|
|
431
|
+
);
|
|
432
|
+
} finally {
|
|
433
|
+
cleanup(base);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
397
437
|
test("dispatch rule ignores failure-path SUMMARY projection when DB milestone is not complete (#4658 superseded)", async () => {
|
|
398
438
|
const state: GSDState = {
|
|
399
439
|
activeMilestone: { id: "M001", title: "Test" },
|