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,434 @@
|
|
|
1
|
+
// GSD-2 + Sync-layer scope variants: tests for ByScope wrappers in auto-worktree.ts
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { createWorkspace, scopeMilestone } from "../workspace.ts";
|
|
17
|
+
import {
|
|
18
|
+
syncProjectRootToWorktree,
|
|
19
|
+
syncProjectRootToWorktreeByScope,
|
|
20
|
+
syncStateToProjectRoot,
|
|
21
|
+
syncStateToProjectRootByScope,
|
|
22
|
+
syncGsdStateToWorktree,
|
|
23
|
+
syncGsdStateToWorktreeByScope,
|
|
24
|
+
} from "../auto-worktree.ts";
|
|
25
|
+
// Phase C: reconcilePlanCheckboxesByScope was deleted along with the
|
|
26
|
+
// underlying reconcilePlanCheckboxes (auto-worktree.ts). Worktrees no
|
|
27
|
+
// longer maintain a parallel .gsd/ projection that needs reconciliation.
|
|
28
|
+
|
|
29
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const MID = "M001-abc123";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a minimal project+worktree layout in a temp dir.
|
|
35
|
+
*
|
|
36
|
+
* Layout:
|
|
37
|
+
* <root>/
|
|
38
|
+
* .gsd/
|
|
39
|
+
* milestones/<MID>/
|
|
40
|
+
* <MID>-CONTEXT.md
|
|
41
|
+
* metrics.json
|
|
42
|
+
* completed-units.json
|
|
43
|
+
* runtime/units/
|
|
44
|
+
* .gsd/worktrees/<MID>/
|
|
45
|
+
* .gsd/ ← worktree-local .gsd projection
|
|
46
|
+
* milestones/<MID>/
|
|
47
|
+
*
|
|
48
|
+
* Returns { projectDir, worktreeDir }.
|
|
49
|
+
*/
|
|
50
|
+
function makeProjectAndWorktree(base: string): {
|
|
51
|
+
projectDir: string;
|
|
52
|
+
worktreeDir: string;
|
|
53
|
+
} {
|
|
54
|
+
const projectDir = realpathSync(base);
|
|
55
|
+
|
|
56
|
+
// Project .gsd layout
|
|
57
|
+
mkdirSync(join(projectDir, ".gsd", "milestones", MID), { recursive: true });
|
|
58
|
+
mkdirSync(join(projectDir, ".gsd", "runtime", "units"), { recursive: true });
|
|
59
|
+
writeFileSync(join(projectDir, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), "context");
|
|
60
|
+
writeFileSync(join(projectDir, ".gsd", "metrics.json"), '{"tokens":0}');
|
|
61
|
+
writeFileSync(join(projectDir, ".gsd", "completed-units.json"), "[]");
|
|
62
|
+
|
|
63
|
+
// Worktree directory inside .gsd/worktrees/<MID> so isGsdWorktreePath recognises it
|
|
64
|
+
const worktreeDir = join(projectDir, ".gsd", "worktrees", MID);
|
|
65
|
+
mkdirSync(join(worktreeDir, ".gsd", "milestones", MID), { recursive: true });
|
|
66
|
+
mkdirSync(join(worktreeDir, ".gsd", "runtime", "units"), { recursive: true });
|
|
67
|
+
|
|
68
|
+
return { projectDir, worktreeDir };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Suite: identity check (throws on mismatched workspace) ─────────────────
|
|
72
|
+
|
|
73
|
+
describe("ByScope variants: mismatched-workspace identity assertion", () => {
|
|
74
|
+
let tmpA: string;
|
|
75
|
+
let tmpB: string;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
tmpA = mkdtempSync(join(tmpdir(), "gsd-sync-scope-a-"));
|
|
79
|
+
tmpB = mkdtempSync(join(tmpdir(), "gsd-sync-scope-b-"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
rmSync(tmpA, { recursive: true, force: true });
|
|
84
|
+
rmSync(tmpB, { recursive: true, force: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("syncProjectRootToWorktreeByScope throws when identityKeys differ", () => {
|
|
88
|
+
mkdirSync(join(tmpA, ".gsd"), { recursive: true });
|
|
89
|
+
mkdirSync(join(tmpB, ".gsd"), { recursive: true });
|
|
90
|
+
const wsA = createWorkspace(tmpA);
|
|
91
|
+
const wsB = createWorkspace(tmpB);
|
|
92
|
+
const scopeA = scopeMilestone(wsA, MID);
|
|
93
|
+
const scopeB = scopeMilestone(wsB, MID);
|
|
94
|
+
|
|
95
|
+
assert.throws(
|
|
96
|
+
() => syncProjectRootToWorktreeByScope(scopeA, scopeB),
|
|
97
|
+
/scope identity mismatch/,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("syncStateToProjectRootByScope throws when identityKeys differ", () => {
|
|
102
|
+
mkdirSync(join(tmpA, ".gsd"), { recursive: true });
|
|
103
|
+
mkdirSync(join(tmpB, ".gsd"), { recursive: true });
|
|
104
|
+
const wsA = createWorkspace(tmpA);
|
|
105
|
+
const wsB = createWorkspace(tmpB);
|
|
106
|
+
const scopeA = scopeMilestone(wsA, MID);
|
|
107
|
+
const scopeB = scopeMilestone(wsB, MID);
|
|
108
|
+
|
|
109
|
+
assert.throws(
|
|
110
|
+
() => syncStateToProjectRootByScope(scopeA, scopeB),
|
|
111
|
+
/scope identity mismatch/,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("syncGsdStateToWorktreeByScope throws when identityKeys differ", () => {
|
|
116
|
+
mkdirSync(join(tmpA, ".gsd"), { recursive: true });
|
|
117
|
+
mkdirSync(join(tmpB, ".gsd"), { recursive: true });
|
|
118
|
+
const wsA = createWorkspace(tmpA);
|
|
119
|
+
const wsB = createWorkspace(tmpB);
|
|
120
|
+
const scopeA = scopeMilestone(wsA, MID);
|
|
121
|
+
const scopeB = scopeMilestone(wsB, MID);
|
|
122
|
+
|
|
123
|
+
assert.throws(
|
|
124
|
+
() => syncGsdStateToWorktreeByScope(scopeA, scopeB),
|
|
125
|
+
/scope identity mismatch/,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Phase C: reconcilePlanCheckboxesByScope identity-mismatch test
|
|
130
|
+
// removed along with the deleted function.
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── Suite: same-milestone, same-workspace path identity ────────────────────
|
|
134
|
+
|
|
135
|
+
describe("ByScope variants: same-workspace produces same paths regardless of scope side", () => {
|
|
136
|
+
let tmp: string;
|
|
137
|
+
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
tmp = mkdtempSync(join(tmpdir(), "gsd-sync-scope-id-"));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("rootScope.workspace.identityKey equals worktreeScope.workspace.identityKey for same project", () => {
|
|
147
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
148
|
+
|
|
149
|
+
const rootWs = createWorkspace(projectDir);
|
|
150
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
151
|
+
|
|
152
|
+
assert.equal(
|
|
153
|
+
rootWs.identityKey,
|
|
154
|
+
worktreeWs.identityKey,
|
|
155
|
+
"both scopes from same project must share identityKey",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rootScope paths and worktreeScope.workspace.projectRoot resolve to the same project root", () => {
|
|
160
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
161
|
+
|
|
162
|
+
const rootWs = createWorkspace(projectDir);
|
|
163
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
164
|
+
|
|
165
|
+
assert.equal(
|
|
166
|
+
rootWs.projectRoot,
|
|
167
|
+
worktreeWs.projectRoot,
|
|
168
|
+
"projectRoot must be identical for root and worktree scopes",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Suite: disk-effect parity (scope variants == legacy path variants) ─────
|
|
174
|
+
|
|
175
|
+
describe("syncProjectRootToWorktreeByScope: disk-effect parity with legacy", () => {
|
|
176
|
+
let tmp1: string;
|
|
177
|
+
let tmp2: string;
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-legacy-"));
|
|
181
|
+
tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-scope-"));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
rmSync(tmp1, { recursive: true, force: true });
|
|
186
|
+
rmSync(tmp2, { recursive: true, force: true });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("scope variant copies milestone dir into worktree identical to legacy variant", () => {
|
|
190
|
+
const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
|
|
191
|
+
const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
|
|
192
|
+
|
|
193
|
+
// Add a source file in project root milestone dir (not yet in worktree)
|
|
194
|
+
const srcFile = "extra-artifact.md";
|
|
195
|
+
writeFileSync(join(proj1, ".gsd", "milestones", MID, srcFile), "artifact");
|
|
196
|
+
writeFileSync(join(proj2, ".gsd", "milestones", MID, srcFile), "artifact");
|
|
197
|
+
|
|
198
|
+
// Remove destination files so something needs to be copied
|
|
199
|
+
rmSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
|
|
200
|
+
rmSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
|
|
201
|
+
|
|
202
|
+
// Run legacy on tmp1, scope variant on tmp2
|
|
203
|
+
syncProjectRootToWorktree(proj1, wt1, MID);
|
|
204
|
+
|
|
205
|
+
const rootWs2 = createWorkspace(proj2);
|
|
206
|
+
const worktreeWs2 = createWorkspace(wt2);
|
|
207
|
+
const rootScope2 = scopeMilestone(rootWs2, MID);
|
|
208
|
+
const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
|
|
209
|
+
syncProjectRootToWorktreeByScope(rootScope2, worktreeScope2);
|
|
210
|
+
|
|
211
|
+
// Both worktrees should now have the CONTEXT.md
|
|
212
|
+
assert.ok(
|
|
213
|
+
existsSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
|
|
214
|
+
"legacy: CONTEXT.md should be copied into worktree",
|
|
215
|
+
);
|
|
216
|
+
assert.ok(
|
|
217
|
+
existsSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
|
|
218
|
+
"scope: CONTEXT.md should be copied into worktree",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("syncStateToProjectRootByScope: disk-effect parity with legacy", () => {
|
|
224
|
+
let tmp1: string;
|
|
225
|
+
let tmp2: string;
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-legacy-"));
|
|
229
|
+
tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-scope-"));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
afterEach(() => {
|
|
233
|
+
rmSync(tmp1, { recursive: true, force: true });
|
|
234
|
+
rmSync(tmp2, { recursive: true, force: true });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("scope variant copies metrics.json from worktree to project root identical to legacy variant", () => {
|
|
238
|
+
const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
|
|
239
|
+
const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
|
|
240
|
+
|
|
241
|
+
// Write metrics.json into each worktree .gsd
|
|
242
|
+
const metricsContent = '{"tokens":42}';
|
|
243
|
+
writeFileSync(join(wt1, ".gsd", "metrics.json"), metricsContent);
|
|
244
|
+
writeFileSync(join(wt2, ".gsd", "metrics.json"), metricsContent);
|
|
245
|
+
|
|
246
|
+
// Run legacy on tmp1, scope variant on tmp2
|
|
247
|
+
syncStateToProjectRoot(wt1, proj1, MID);
|
|
248
|
+
|
|
249
|
+
const rootWs2 = createWorkspace(proj2);
|
|
250
|
+
const worktreeWs2 = createWorkspace(wt2);
|
|
251
|
+
const rootScope2 = scopeMilestone(rootWs2, MID);
|
|
252
|
+
const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
|
|
253
|
+
syncStateToProjectRootByScope(worktreeScope2, rootScope2);
|
|
254
|
+
|
|
255
|
+
// Both project roots should now have the updated metrics.json
|
|
256
|
+
assert.ok(
|
|
257
|
+
existsSync(join(proj1, ".gsd", "metrics.json")),
|
|
258
|
+
"legacy: metrics.json should be synced to project root",
|
|
259
|
+
);
|
|
260
|
+
assert.ok(
|
|
261
|
+
existsSync(join(proj2, ".gsd", "metrics.json")),
|
|
262
|
+
"scope: metrics.json should be synced to project root",
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("syncGsdStateToWorktreeByScope: disk-effect parity with legacy", () => {
|
|
268
|
+
let tmp1: string;
|
|
269
|
+
let tmp2: string;
|
|
270
|
+
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-legacy-"));
|
|
273
|
+
tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-scope-"));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
rmSync(tmp1, { recursive: true, force: true });
|
|
278
|
+
rmSync(tmp2, { recursive: true, force: true });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("scope variant syncs root state files into worktree identical to legacy variant", () => {
|
|
282
|
+
const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
|
|
283
|
+
const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
|
|
284
|
+
|
|
285
|
+
// Add a root state file in each project .gsd (not yet in worktree)
|
|
286
|
+
writeFileSync(join(proj1, ".gsd", "DECISIONS.md"), "decisions");
|
|
287
|
+
writeFileSync(join(proj2, ".gsd", "DECISIONS.md"), "decisions");
|
|
288
|
+
|
|
289
|
+
// Run legacy on tmp1, scope variant on tmp2
|
|
290
|
+
syncGsdStateToWorktree(proj1, wt1);
|
|
291
|
+
|
|
292
|
+
const rootWs2 = createWorkspace(proj2);
|
|
293
|
+
const worktreeWs2 = createWorkspace(wt2);
|
|
294
|
+
const rootScope2 = scopeMilestone(rootWs2, MID);
|
|
295
|
+
const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
|
|
296
|
+
syncGsdStateToWorktreeByScope(rootScope2, worktreeScope2);
|
|
297
|
+
|
|
298
|
+
// Both worktrees should now have DECISIONS.md
|
|
299
|
+
assert.ok(
|
|
300
|
+
existsSync(join(wt1, ".gsd", "DECISIONS.md")),
|
|
301
|
+
"legacy: DECISIONS.md should be copied into worktree",
|
|
302
|
+
);
|
|
303
|
+
assert.ok(
|
|
304
|
+
existsSync(join(wt2, ".gsd", "DECISIONS.md")),
|
|
305
|
+
"scope: DECISIONS.md should be copied into worktree",
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ─── Suite: direction tests ──────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
describe("sync direction: project→worktree variants only write to worktree side", () => {
|
|
313
|
+
let tmp: string;
|
|
314
|
+
|
|
315
|
+
beforeEach(() => {
|
|
316
|
+
tmp = mkdtempSync(join(tmpdir(), "gsd-sync-dir-"));
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
afterEach(() => {
|
|
320
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("syncProjectRootToWorktreeByScope: new file appears in worktree, not duplicated to project root", () => {
|
|
324
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
325
|
+
|
|
326
|
+
// New file in project root milestone — not yet in worktree
|
|
327
|
+
const marker = "direction-test.md";
|
|
328
|
+
writeFileSync(join(projectDir, ".gsd", "milestones", MID, marker), "marker");
|
|
329
|
+
|
|
330
|
+
// Remove from worktree so we can detect it being added
|
|
331
|
+
const wtDst = join(worktreeDir, ".gsd", "milestones", MID, marker);
|
|
332
|
+
rmSync(wtDst, { force: true });
|
|
333
|
+
|
|
334
|
+
const rootWs = createWorkspace(projectDir);
|
|
335
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
336
|
+
const rootScope = scopeMilestone(rootWs, MID);
|
|
337
|
+
const worktreeScope = scopeMilestone(worktreeWs, MID);
|
|
338
|
+
|
|
339
|
+
syncProjectRootToWorktreeByScope(rootScope, worktreeScope);
|
|
340
|
+
|
|
341
|
+
// File should now be in worktree
|
|
342
|
+
assert.ok(existsSync(wtDst), "marker file should appear in worktree after project→worktree sync");
|
|
343
|
+
|
|
344
|
+
// The original in project root should still be there (not removed)
|
|
345
|
+
assert.ok(
|
|
346
|
+
existsSync(join(projectDir, ".gsd", "milestones", MID, marker)),
|
|
347
|
+
"project root marker file should not be removed",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("syncStateToProjectRootByScope: new file appears in project root from worktree", () => {
|
|
352
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
353
|
+
|
|
354
|
+
// Write a runtime unit file into worktree
|
|
355
|
+
const unitFile = "some-unit-M001.json";
|
|
356
|
+
writeFileSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile), '{"status":"done"}');
|
|
357
|
+
|
|
358
|
+
const rootWs = createWorkspace(projectDir);
|
|
359
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
360
|
+
const rootScope = scopeMilestone(rootWs, MID);
|
|
361
|
+
const worktreeScope = scopeMilestone(worktreeWs, MID);
|
|
362
|
+
|
|
363
|
+
syncStateToProjectRootByScope(worktreeScope, rootScope);
|
|
364
|
+
|
|
365
|
+
// Unit file should now be in project root
|
|
366
|
+
assert.ok(
|
|
367
|
+
existsSync(join(projectDir, ".gsd", "runtime", "units", unitFile)),
|
|
368
|
+
"runtime unit file should appear in project root after worktree→root sync",
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Worktree side should still have the file (not removed)
|
|
372
|
+
assert.ok(
|
|
373
|
+
existsSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile)),
|
|
374
|
+
"worktree runtime unit file should not be removed",
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ─── Suite: milestoneId mismatch guard ───────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
describe("ByScope variants: milestoneId mismatch throws for milestone-aware wrappers", () => {
|
|
382
|
+
let tmp: string;
|
|
383
|
+
|
|
384
|
+
beforeEach(() => {
|
|
385
|
+
tmp = mkdtempSync(join(tmpdir(), "gsd-sync-mid-mismatch-"));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
afterEach(() => {
|
|
389
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("syncProjectRootToWorktreeByScope throws when milestoneIds differ", () => {
|
|
393
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
394
|
+
const rootWs = createWorkspace(projectDir);
|
|
395
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
396
|
+
// Same workspace identity, different milestoneId
|
|
397
|
+
const rootScope = scopeMilestone(rootWs, "M001-abc123");
|
|
398
|
+
const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
|
|
399
|
+
|
|
400
|
+
assert.throws(
|
|
401
|
+
() => syncProjectRootToWorktreeByScope(rootScope, worktreeScope),
|
|
402
|
+
/milestoneId mismatch/,
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("syncStateToProjectRootByScope throws when milestoneIds differ", () => {
|
|
407
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
408
|
+
const rootWs = createWorkspace(projectDir);
|
|
409
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
410
|
+
const rootScope = scopeMilestone(rootWs, "M001-abc123");
|
|
411
|
+
const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
|
|
412
|
+
|
|
413
|
+
assert.throws(
|
|
414
|
+
() => syncStateToProjectRootByScope(worktreeScope, rootScope),
|
|
415
|
+
/milestoneId mismatch/,
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Phase C: reconcilePlanCheckboxesByScope milestoneId-mismatch test
|
|
420
|
+
// removed along with the deleted function.
|
|
421
|
+
|
|
422
|
+
test("syncGsdStateToWorktreeByScope does NOT throw when milestoneIds differ (workspace-only wrapper)", () => {
|
|
423
|
+
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
424
|
+
const rootWs = createWorkspace(projectDir);
|
|
425
|
+
const worktreeWs = createWorkspace(worktreeDir);
|
|
426
|
+
// Different milestoneIds — syncGsdStateToWorktreeByScope must not guard milestoneId
|
|
427
|
+
const rootScope = scopeMilestone(rootWs, "M001-abc123");
|
|
428
|
+
const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
|
|
429
|
+
|
|
430
|
+
assert.doesNotThrow(
|
|
431
|
+
() => syncGsdStateToWorktreeByScope(rootScope, worktreeScope),
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// GSD-2 + Regression test: teardownAutoWorktree clears activeWorkspace even when process.chdir throws
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression (H3 broadened scope): `teardownAutoWorktree` must clear `activeWorkspace`
|
|
5
|
+
* (and therefore `getAutoWorktreeOriginalBase()` / `getActiveAutoWorktreeContext()`)
|
|
6
|
+
* unconditionally — regardless of where in the function body an error occurs.
|
|
7
|
+
*
|
|
8
|
+
* The original H3 fix (d1276b021) wrapped only `removeWorktree(...)` in a
|
|
9
|
+
* try/finally. But `process.chdir(originalBasePath)` at the top of the function
|
|
10
|
+
* can throw a GSDError if the target directory no longer exists. In that case
|
|
11
|
+
* execution exits the function before ever reaching the inner try/finally, leaving
|
|
12
|
+
* `activeWorkspace` stale.
|
|
13
|
+
*
|
|
14
|
+
* The fix: a single outer try/finally wraps the entire teardown body so
|
|
15
|
+
* `setActiveWorkspace(null)` runs regardless of which step throws.
|
|
16
|
+
*
|
|
17
|
+
* Test strategy:
|
|
18
|
+
* 1. Populate the registry via `createAutoWorktree` on a real temp git repo.
|
|
19
|
+
* 2. Delete the repo directory so `process.chdir(originalBasePath)` throws.
|
|
20
|
+
* 3. Assert `teardownAutoWorktree` re-throws (chdir failure still propagates).
|
|
21
|
+
* 4. Assert `getActiveWorkspace()` is null — the broadened finally caught it.
|
|
22
|
+
* 5. Regression: success path still clears activeWorkspace (same guarantee).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import {
|
|
28
|
+
mkdirSync,
|
|
29
|
+
mkdtempSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
realpathSync,
|
|
33
|
+
existsSync,
|
|
34
|
+
} from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { tmpdir } from "node:os";
|
|
37
|
+
import { execFileSync } from "node:child_process";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
createAutoWorktree,
|
|
41
|
+
teardownAutoWorktree,
|
|
42
|
+
getAutoWorktreeOriginalBase,
|
|
43
|
+
getActiveAutoWorktreeContext,
|
|
44
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
45
|
+
} from "../auto-worktree.ts";
|
|
46
|
+
|
|
47
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function git(args: string[], cwd: string): void {
|
|
50
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createTempRepo(): string {
|
|
54
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-chdir-fail-")));
|
|
55
|
+
git(["init"], dir);
|
|
56
|
+
git(["config", "user.email", "test@gsd.test"], dir);
|
|
57
|
+
git(["config", "user.name", "Test"], dir);
|
|
58
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
59
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
60
|
+
git(["add", "."], dir);
|
|
61
|
+
git(["commit", "-m", "init"], dir);
|
|
62
|
+
git(["branch", "-M", "main"], dir);
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function seedMilestone(repoDir: string, milestoneId: string): void {
|
|
67
|
+
const msDir = join(repoDir, ".gsd", "milestones", milestoneId);
|
|
68
|
+
mkdirSync(msDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(msDir, "CONTEXT.md"), `# ${milestoneId} Context\n`);
|
|
70
|
+
git(["add", "."], repoDir);
|
|
71
|
+
git(["commit", "-m", `add ${milestoneId}`], repoDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("teardown chdir failure clears registry", () => {
|
|
77
|
+
const savedCwd = process.cwd();
|
|
78
|
+
let repoDir: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
82
|
+
process.chdir(savedCwd);
|
|
83
|
+
repoDir = "";
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
88
|
+
process.chdir(savedCwd);
|
|
89
|
+
if (repoDir && existsSync(repoDir)) {
|
|
90
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
repoDir = "";
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── chdir failure path (the new coverage) ──────────────────────────────────
|
|
96
|
+
|
|
97
|
+
test("registry is null after teardown throws due to chdir failure (H3 broadened scope)", () => {
|
|
98
|
+
repoDir = createTempRepo();
|
|
99
|
+
seedMilestone(repoDir, "M001");
|
|
100
|
+
|
|
101
|
+
// Populate the registry by entering the worktree
|
|
102
|
+
createAutoWorktree(repoDir, "M001");
|
|
103
|
+
|
|
104
|
+
assert.strictEqual(
|
|
105
|
+
getAutoWorktreeOriginalBase(),
|
|
106
|
+
repoDir,
|
|
107
|
+
"registry is populated after createAutoWorktree",
|
|
108
|
+
);
|
|
109
|
+
assert.notStrictEqual(
|
|
110
|
+
getActiveAutoWorktreeContext(),
|
|
111
|
+
null,
|
|
112
|
+
"context is non-null after createAutoWorktree",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Move back to a safe cwd so we can delete the repo dir
|
|
116
|
+
process.chdir(savedCwd);
|
|
117
|
+
|
|
118
|
+
// Delete the repo directory — process.chdir(repoDir) inside teardown will throw
|
|
119
|
+
const capturedRepoDir = repoDir;
|
|
120
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
121
|
+
repoDir = ""; // afterEach cleanup no longer needed
|
|
122
|
+
|
|
123
|
+
// teardownAutoWorktree must throw (chdir to deleted dir fails)
|
|
124
|
+
assert.throws(
|
|
125
|
+
() => teardownAutoWorktree(capturedRepoDir, "M001"),
|
|
126
|
+
"teardownAutoWorktree should throw when originalBasePath does not exist",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// The broadened outer finally must have cleared the registry despite the throw
|
|
130
|
+
assert.strictEqual(
|
|
131
|
+
getAutoWorktreeOriginalBase(),
|
|
132
|
+
null,
|
|
133
|
+
"getAutoWorktreeOriginalBase() is null after chdir-failure teardown (H3)",
|
|
134
|
+
);
|
|
135
|
+
assert.strictEqual(
|
|
136
|
+
getActiveAutoWorktreeContext(),
|
|
137
|
+
null,
|
|
138
|
+
"getActiveAutoWorktreeContext() is null after chdir-failure teardown (H3)",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── Success path (regression guard) ───────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
test("registry is null after successful teardown (success path regression)", () => {
|
|
145
|
+
repoDir = createTempRepo();
|
|
146
|
+
seedMilestone(repoDir, "M002");
|
|
147
|
+
|
|
148
|
+
// Confirm baseline
|
|
149
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null before entering worktree");
|
|
150
|
+
|
|
151
|
+
createAutoWorktree(repoDir, "M002");
|
|
152
|
+
|
|
153
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir, "registry set after createAutoWorktree");
|
|
154
|
+
assert.notStrictEqual(getActiveAutoWorktreeContext(), null, "context non-null after createAutoWorktree");
|
|
155
|
+
|
|
156
|
+
// Normal teardown — finally block must still clear registry
|
|
157
|
+
teardownAutoWorktree(repoDir, "M002");
|
|
158
|
+
|
|
159
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null after successful teardown");
|
|
160
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null, "context null after successful teardown");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Auto-Worktree -- teardown-cleanup-parity.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Regression test: teardownAutoWorktree (abort path) must call
|
|
5
|
+
* clearProjectRootStateFiles, removing STATE.md, auto.lock, and
|
|
6
|
+
* {MID}-META.json from the project root .gsd/ dir.
|
|
7
|
+
*
|
|
8
|
+
* Prior to the fix these files were left behind on disk after abort teardown.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import {
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
realpathSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
|
|
25
|
+
import { teardownAutoWorktree, _resetAutoWorktreeOriginalBaseForTests } from "../auto-worktree.ts";
|
|
26
|
+
|
|
27
|
+
function git(args: string[], cwd: string): void {
|
|
28
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createTempRepo(): string {
|
|
32
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-teardown-parity-")));
|
|
33
|
+
git(["init"], dir);
|
|
34
|
+
git(["config", "user.email", "test@gsd.test"], dir);
|
|
35
|
+
git(["config", "user.name", "Test"], dir);
|
|
36
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
37
|
+
git(["add", "README.md"], dir);
|
|
38
|
+
git(["commit", "-m", "init"], dir);
|
|
39
|
+
git(["branch", "-M", "main"], dir);
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("teardownAutoWorktree cleanup parity", () => {
|
|
44
|
+
let repoDir: string;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
repoDir = createTempRepo();
|
|
48
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
53
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("STATE.md and M001-META.json are removed after abort teardown", () => {
|
|
57
|
+
// Phase C pt 2: auto.lock no longer exists as a file — it migrated
|
|
58
|
+
// to the workers + unit_dispatches tables. clearProjectRootStateFiles
|
|
59
|
+
// still removes STATE.md and {MID}-META.json on teardown.
|
|
60
|
+
const gsdDir = join(repoDir, ".gsd");
|
|
61
|
+
const milestonesDir = join(gsdDir, "milestones", "M001");
|
|
62
|
+
mkdirSync(milestonesDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const stateMd = join(gsdDir, "STATE.md");
|
|
65
|
+
const metaJson = join(milestonesDir, "M001-META.json");
|
|
66
|
+
|
|
67
|
+
writeFileSync(stateMd, "# State\nactive\n");
|
|
68
|
+
writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
|
|
69
|
+
|
|
70
|
+
assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
|
|
71
|
+
assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
|
|
72
|
+
|
|
73
|
+
// teardownAutoWorktree may throw when git worktree removal fails
|
|
74
|
+
// (no actual worktree was created), but clearProjectRootStateFiles
|
|
75
|
+
// runs before removeWorktree so the state files must be gone regardless.
|
|
76
|
+
try {
|
|
77
|
+
teardownAutoWorktree(repoDir, "M001");
|
|
78
|
+
} catch {
|
|
79
|
+
// git teardown may fail in a minimal test repo — that is acceptable
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree");
|
|
83
|
+
assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("teardown is non-fatal when state files do not exist", () => {
|
|
87
|
+
// No state files created — teardown should not throw due to missing files
|
|
88
|
+
// (clearProjectRootStateFiles tolerates ENOENT).
|
|
89
|
+
try {
|
|
90
|
+
teardownAutoWorktree(repoDir, "M001");
|
|
91
|
+
} catch {
|
|
92
|
+
// git teardown may fail — acceptable
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Reaching here means clearProjectRootStateFiles did not throw for missing files.
|
|
96
|
+
assert.ok(true, "teardown with missing state files did not throw from clearProjectRootStateFiles");
|
|
97
|
+
});
|
|
98
|
+
});
|