gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
WorktreeResolver,
|
|
5
|
+
type WorktreeResolverDeps,
|
|
6
|
+
type NotifyCtx,
|
|
7
|
+
} from "../worktree-resolver.js";
|
|
8
|
+
import { AutoSession } from "../auto/session.js";
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Track calls to mock deps for assertion. */
|
|
13
|
+
interface CallLog {
|
|
14
|
+
fn: string;
|
|
15
|
+
args: unknown[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeSession(
|
|
19
|
+
overrides?: Partial<{ basePath: string; originalBasePath: string }>,
|
|
20
|
+
): AutoSession {
|
|
21
|
+
const s = new AutoSession();
|
|
22
|
+
s.basePath = overrides?.basePath ?? "/project";
|
|
23
|
+
s.originalBasePath = overrides?.originalBasePath ?? "/project";
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeDeps(
|
|
28
|
+
overrides?: Partial<WorktreeResolverDeps>,
|
|
29
|
+
): WorktreeResolverDeps & { calls: CallLog[] } {
|
|
30
|
+
const calls: CallLog[] = [];
|
|
31
|
+
|
|
32
|
+
const deps: WorktreeResolverDeps & { calls: CallLog[] } = {
|
|
33
|
+
calls,
|
|
34
|
+
isInAutoWorktree: (basePath: string) => {
|
|
35
|
+
calls.push({ fn: "isInAutoWorktree", args: [basePath] });
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
shouldUseWorktreeIsolation: () => {
|
|
39
|
+
calls.push({ fn: "shouldUseWorktreeIsolation", args: [] });
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
getIsolationMode: () => {
|
|
43
|
+
calls.push({ fn: "getIsolationMode", args: [] });
|
|
44
|
+
return "worktree";
|
|
45
|
+
},
|
|
46
|
+
mergeMilestoneToMain: (
|
|
47
|
+
basePath: string,
|
|
48
|
+
milestoneId: string,
|
|
49
|
+
roadmapContent: string,
|
|
50
|
+
) => {
|
|
51
|
+
calls.push({
|
|
52
|
+
fn: "mergeMilestoneToMain",
|
|
53
|
+
args: [basePath, milestoneId, roadmapContent],
|
|
54
|
+
});
|
|
55
|
+
return { pushed: false };
|
|
56
|
+
},
|
|
57
|
+
syncWorktreeStateBack: (
|
|
58
|
+
mainBasePath: string,
|
|
59
|
+
worktreePath: string,
|
|
60
|
+
milestoneId: string,
|
|
61
|
+
) => {
|
|
62
|
+
calls.push({
|
|
63
|
+
fn: "syncWorktreeStateBack",
|
|
64
|
+
args: [mainBasePath, worktreePath, milestoneId],
|
|
65
|
+
});
|
|
66
|
+
return { synced: [] };
|
|
67
|
+
},
|
|
68
|
+
teardownAutoWorktree: (
|
|
69
|
+
basePath: string,
|
|
70
|
+
milestoneId: string,
|
|
71
|
+
opts?: { preserveBranch?: boolean },
|
|
72
|
+
) => {
|
|
73
|
+
calls.push({
|
|
74
|
+
fn: "teardownAutoWorktree",
|
|
75
|
+
args: [basePath, milestoneId, opts],
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
createAutoWorktree: (basePath: string, milestoneId: string) => {
|
|
79
|
+
calls.push({ fn: "createAutoWorktree", args: [basePath, milestoneId] });
|
|
80
|
+
return `/project/.gsd/worktrees/${milestoneId}`;
|
|
81
|
+
},
|
|
82
|
+
enterAutoWorktree: (basePath: string, milestoneId: string) => {
|
|
83
|
+
calls.push({ fn: "enterAutoWorktree", args: [basePath, milestoneId] });
|
|
84
|
+
return `/project/.gsd/worktrees/${milestoneId}`;
|
|
85
|
+
},
|
|
86
|
+
getAutoWorktreePath: (basePath: string, milestoneId: string) => {
|
|
87
|
+
calls.push({ fn: "getAutoWorktreePath", args: [basePath, milestoneId] });
|
|
88
|
+
return null;
|
|
89
|
+
},
|
|
90
|
+
autoCommitCurrentBranch: (
|
|
91
|
+
basePath: string,
|
|
92
|
+
reason: string,
|
|
93
|
+
milestoneId: string,
|
|
94
|
+
) => {
|
|
95
|
+
calls.push({
|
|
96
|
+
fn: "autoCommitCurrentBranch",
|
|
97
|
+
args: [basePath, reason, milestoneId],
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
getCurrentBranch: (basePath: string) => {
|
|
101
|
+
calls.push({ fn: "getCurrentBranch", args: [basePath] });
|
|
102
|
+
return "main";
|
|
103
|
+
},
|
|
104
|
+
autoWorktreeBranch: (milestoneId: string) => {
|
|
105
|
+
calls.push({ fn: "autoWorktreeBranch", args: [milestoneId] });
|
|
106
|
+
return `milestone/${milestoneId}`;
|
|
107
|
+
},
|
|
108
|
+
resolveMilestoneFile: (
|
|
109
|
+
basePath: string,
|
|
110
|
+
milestoneId: string,
|
|
111
|
+
fileType: string,
|
|
112
|
+
) => {
|
|
113
|
+
calls.push({
|
|
114
|
+
fn: "resolveMilestoneFile",
|
|
115
|
+
args: [basePath, milestoneId, fileType],
|
|
116
|
+
});
|
|
117
|
+
return `/project/.gsd/milestones/${milestoneId}/${milestoneId}-ROADMAP.md`;
|
|
118
|
+
},
|
|
119
|
+
readFileSync: (path: string, _encoding: string) => {
|
|
120
|
+
calls.push({ fn: "readFileSync", args: [path] });
|
|
121
|
+
return "# Roadmap\n- [x] S01: Slice one\n";
|
|
122
|
+
},
|
|
123
|
+
GitServiceImpl: class MockGitServiceImpl {
|
|
124
|
+
basePath: string;
|
|
125
|
+
gitConfig: unknown;
|
|
126
|
+
constructor(basePath: string, gitConfig: unknown) {
|
|
127
|
+
calls.push({ fn: "GitServiceImpl", args: [basePath, gitConfig] });
|
|
128
|
+
this.basePath = basePath;
|
|
129
|
+
this.gitConfig = gitConfig;
|
|
130
|
+
}
|
|
131
|
+
} as unknown as WorktreeResolverDeps["GitServiceImpl"],
|
|
132
|
+
loadEffectiveGSDPreferences: () => {
|
|
133
|
+
calls.push({ fn: "loadEffectiveGSDPreferences", args: [] });
|
|
134
|
+
return { preferences: { git: {} } };
|
|
135
|
+
},
|
|
136
|
+
invalidateAllCaches: () => {
|
|
137
|
+
calls.push({ fn: "invalidateAllCaches", args: [] });
|
|
138
|
+
},
|
|
139
|
+
captureIntegrationBranch: (
|
|
140
|
+
basePath: string,
|
|
141
|
+
mid: string | undefined,
|
|
142
|
+
opts?: { commitDocs?: boolean },
|
|
143
|
+
) => {
|
|
144
|
+
calls.push({
|
|
145
|
+
fn: "captureIntegrationBranch",
|
|
146
|
+
args: [basePath, mid, opts],
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
...overrides,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Re-apply overrides that add the call tracking
|
|
153
|
+
if (overrides) {
|
|
154
|
+
for (const [key, val] of Object.entries(overrides)) {
|
|
155
|
+
if (key !== "calls") {
|
|
156
|
+
(deps as unknown as Record<string, unknown>)[key] = val;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return deps;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeNotifyCtx(): NotifyCtx & {
|
|
165
|
+
messages: Array<{ msg: string; level?: string }>;
|
|
166
|
+
} {
|
|
167
|
+
const messages: Array<{ msg: string; level?: string }> = [];
|
|
168
|
+
return {
|
|
169
|
+
messages,
|
|
170
|
+
notify: (msg: string, level?: "info" | "warning" | "error" | "success") => {
|
|
171
|
+
messages.push({ msg, level });
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function findCalls(calls: CallLog[], fn: string): CallLog[] {
|
|
177
|
+
return calls.filter((c) => c.fn === fn);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Getter Tests ────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
test("workPath returns s.basePath", () => {
|
|
183
|
+
const s = makeSession({ basePath: "/project/.gsd/worktrees/M001" });
|
|
184
|
+
const resolver = new WorktreeResolver(s, makeDeps());
|
|
185
|
+
assert.equal(resolver.workPath, "/project/.gsd/worktrees/M001");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("projectRoot returns originalBasePath when set", () => {
|
|
189
|
+
const s = makeSession({
|
|
190
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
191
|
+
originalBasePath: "/project",
|
|
192
|
+
});
|
|
193
|
+
const resolver = new WorktreeResolver(s, makeDeps());
|
|
194
|
+
assert.equal(resolver.projectRoot, "/project");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("projectRoot falls back to basePath when originalBasePath is empty", () => {
|
|
198
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "" });
|
|
199
|
+
const resolver = new WorktreeResolver(s, makeDeps());
|
|
200
|
+
assert.equal(resolver.projectRoot, "/project");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("lockPath returns originalBasePath when set (same as lockBase)", () => {
|
|
204
|
+
const s = makeSession({
|
|
205
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
206
|
+
originalBasePath: "/project",
|
|
207
|
+
});
|
|
208
|
+
const resolver = new WorktreeResolver(s, makeDeps());
|
|
209
|
+
assert.equal(resolver.lockPath, "/project");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("lockPath falls back to basePath when originalBasePath is empty", () => {
|
|
213
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "" });
|
|
214
|
+
const resolver = new WorktreeResolver(s, makeDeps());
|
|
215
|
+
assert.equal(resolver.lockPath, "/project");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ─── enterMilestone Tests ────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
test("enterMilestone creates new worktree when none exists", () => {
|
|
221
|
+
const s = makeSession();
|
|
222
|
+
const deps = makeDeps({
|
|
223
|
+
getAutoWorktreePath: () => null,
|
|
224
|
+
});
|
|
225
|
+
const ctx = makeNotifyCtx();
|
|
226
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
227
|
+
|
|
228
|
+
resolver.enterMilestone("M001", ctx);
|
|
229
|
+
|
|
230
|
+
assert.equal(s.basePath, "/project/.gsd/worktrees/M001");
|
|
231
|
+
assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 1);
|
|
232
|
+
assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
|
|
233
|
+
assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
|
|
234
|
+
assert.ok(
|
|
235
|
+
ctx.messages.some(
|
|
236
|
+
(m) => m.level === "info" && m.msg.includes("Entered worktree"),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("enterMilestone enters existing worktree instead of creating", () => {
|
|
242
|
+
const s = makeSession();
|
|
243
|
+
const deps = makeDeps({
|
|
244
|
+
getAutoWorktreePath: () => "/project/.gsd/worktrees/M001",
|
|
245
|
+
});
|
|
246
|
+
const ctx = makeNotifyCtx();
|
|
247
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
248
|
+
|
|
249
|
+
resolver.enterMilestone("M001", ctx);
|
|
250
|
+
|
|
251
|
+
assert.equal(s.basePath, "/project/.gsd/worktrees/M001");
|
|
252
|
+
assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 1);
|
|
253
|
+
assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("enterMilestone is no-op when shouldUseWorktreeIsolation is false", () => {
|
|
257
|
+
const s = makeSession();
|
|
258
|
+
const deps = makeDeps({
|
|
259
|
+
shouldUseWorktreeIsolation: () => false,
|
|
260
|
+
});
|
|
261
|
+
const ctx = makeNotifyCtx();
|
|
262
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
263
|
+
|
|
264
|
+
resolver.enterMilestone("M001", ctx);
|
|
265
|
+
|
|
266
|
+
assert.equal(s.basePath, "/project"); // unchanged
|
|
267
|
+
assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
|
|
268
|
+
assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("enterMilestone does NOT update basePath on creation failure", () => {
|
|
272
|
+
const s = makeSession();
|
|
273
|
+
const deps = makeDeps({
|
|
274
|
+
getAutoWorktreePath: () => null,
|
|
275
|
+
createAutoWorktree: () => {
|
|
276
|
+
throw new Error("disk full");
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const ctx = makeNotifyCtx();
|
|
280
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
281
|
+
|
|
282
|
+
resolver.enterMilestone("M001", ctx);
|
|
283
|
+
|
|
284
|
+
assert.equal(s.basePath, "/project"); // unchanged — error recovery
|
|
285
|
+
assert.ok(
|
|
286
|
+
ctx.messages.some(
|
|
287
|
+
(m) => m.level === "warning" && m.msg.includes("disk full"),
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("enterMilestone uses originalBasePath as base for worktree ops", () => {
|
|
293
|
+
const s = makeSession({
|
|
294
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
295
|
+
originalBasePath: "/project",
|
|
296
|
+
});
|
|
297
|
+
let createdFrom = "";
|
|
298
|
+
const deps = makeDeps({
|
|
299
|
+
getAutoWorktreePath: () => null,
|
|
300
|
+
createAutoWorktree: (basePath: string, _mid: string) => {
|
|
301
|
+
createdFrom = basePath;
|
|
302
|
+
return "/project/.gsd/worktrees/M002";
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const ctx = makeNotifyCtx();
|
|
306
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
307
|
+
|
|
308
|
+
resolver.enterMilestone("M002", ctx);
|
|
309
|
+
|
|
310
|
+
assert.equal(createdFrom, "/project"); // uses originalBasePath, not current basePath
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── exitMilestone Tests ─────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
test("exitMilestone commits, tears down, and resets basePath", () => {
|
|
316
|
+
const s = makeSession({
|
|
317
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
318
|
+
originalBasePath: "/project",
|
|
319
|
+
});
|
|
320
|
+
const deps = makeDeps({
|
|
321
|
+
isInAutoWorktree: () => true,
|
|
322
|
+
});
|
|
323
|
+
const ctx = makeNotifyCtx();
|
|
324
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
325
|
+
|
|
326
|
+
resolver.exitMilestone("M001", ctx);
|
|
327
|
+
|
|
328
|
+
assert.equal(s.basePath, "/project"); // reset to originalBasePath
|
|
329
|
+
assert.equal(findCalls(deps.calls, "autoCommitCurrentBranch").length, 1);
|
|
330
|
+
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 1);
|
|
331
|
+
assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1); // rebuilt
|
|
332
|
+
assert.equal(findCalls(deps.calls, "invalidateAllCaches").length, 1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("exitMilestone is no-op when not in worktree", () => {
|
|
336
|
+
const s = makeSession();
|
|
337
|
+
const deps = makeDeps({
|
|
338
|
+
isInAutoWorktree: () => false,
|
|
339
|
+
});
|
|
340
|
+
const ctx = makeNotifyCtx();
|
|
341
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
342
|
+
|
|
343
|
+
resolver.exitMilestone("M001", ctx);
|
|
344
|
+
|
|
345
|
+
assert.equal(s.basePath, "/project"); // unchanged
|
|
346
|
+
assert.equal(findCalls(deps.calls, "autoCommitCurrentBranch").length, 0);
|
|
347
|
+
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("exitMilestone passes preserveBranch option", () => {
|
|
351
|
+
const s = makeSession({
|
|
352
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
353
|
+
originalBasePath: "/project",
|
|
354
|
+
});
|
|
355
|
+
let preserveOpts: unknown = null;
|
|
356
|
+
const deps = makeDeps({
|
|
357
|
+
isInAutoWorktree: () => true,
|
|
358
|
+
teardownAutoWorktree: (
|
|
359
|
+
_basePath: string,
|
|
360
|
+
_mid: string,
|
|
361
|
+
opts?: { preserveBranch?: boolean },
|
|
362
|
+
) => {
|
|
363
|
+
preserveOpts = opts;
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
const ctx = makeNotifyCtx();
|
|
367
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
368
|
+
|
|
369
|
+
resolver.exitMilestone("M001", ctx, { preserveBranch: true });
|
|
370
|
+
|
|
371
|
+
assert.deepEqual(preserveOpts, { preserveBranch: true });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("exitMilestone still resets basePath even if auto-commit fails", () => {
|
|
375
|
+
const s = makeSession({
|
|
376
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
377
|
+
originalBasePath: "/project",
|
|
378
|
+
});
|
|
379
|
+
const deps = makeDeps({
|
|
380
|
+
isInAutoWorktree: () => true,
|
|
381
|
+
autoCommitCurrentBranch: () => {
|
|
382
|
+
throw new Error("commit error");
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
const ctx = makeNotifyCtx();
|
|
386
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
387
|
+
|
|
388
|
+
resolver.exitMilestone("M001", ctx);
|
|
389
|
+
|
|
390
|
+
// Should still complete: reset basePath, rebuild git service
|
|
391
|
+
assert.equal(s.basePath, "/project");
|
|
392
|
+
assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ─── mergeAndExit Tests (worktree mode) ──────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
test("mergeAndExit in worktree mode reads roadmap and merges", () => {
|
|
398
|
+
const s = makeSession({
|
|
399
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
400
|
+
originalBasePath: "/project",
|
|
401
|
+
});
|
|
402
|
+
const deps = makeDeps({
|
|
403
|
+
isInAutoWorktree: () => true,
|
|
404
|
+
getIsolationMode: () => "worktree",
|
|
405
|
+
});
|
|
406
|
+
const ctx = makeNotifyCtx();
|
|
407
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
408
|
+
|
|
409
|
+
resolver.mergeAndExit("M001", ctx);
|
|
410
|
+
|
|
411
|
+
assert.equal(findCalls(deps.calls, "syncWorktreeStateBack").length, 1);
|
|
412
|
+
assert.equal(findCalls(deps.calls, "resolveMilestoneFile").length, 1);
|
|
413
|
+
assert.equal(findCalls(deps.calls, "readFileSync").length, 1);
|
|
414
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
|
|
415
|
+
assert.equal(s.basePath, "/project"); // restored
|
|
416
|
+
assert.ok(ctx.messages.some((m) => m.msg.includes("merged to main")));
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("mergeAndExit in worktree mode shows pushed status", () => {
|
|
420
|
+
const s = makeSession({
|
|
421
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
422
|
+
originalBasePath: "/project",
|
|
423
|
+
});
|
|
424
|
+
const deps = makeDeps({
|
|
425
|
+
isInAutoWorktree: () => true,
|
|
426
|
+
getIsolationMode: () => "worktree",
|
|
427
|
+
mergeMilestoneToMain: () => ({ pushed: true }),
|
|
428
|
+
});
|
|
429
|
+
const ctx = makeNotifyCtx();
|
|
430
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
431
|
+
|
|
432
|
+
resolver.mergeAndExit("M001", ctx);
|
|
433
|
+
|
|
434
|
+
assert.ok(ctx.messages.some((m) => m.msg.includes("Pushed to remote")));
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("mergeAndExit falls back to teardown when roadmap is missing", () => {
|
|
438
|
+
const s = makeSession({
|
|
439
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
440
|
+
originalBasePath: "/project",
|
|
441
|
+
});
|
|
442
|
+
const deps = makeDeps({
|
|
443
|
+
isInAutoWorktree: () => true,
|
|
444
|
+
getIsolationMode: () => "worktree",
|
|
445
|
+
resolveMilestoneFile: () => null,
|
|
446
|
+
});
|
|
447
|
+
const ctx = makeNotifyCtx();
|
|
448
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
449
|
+
|
|
450
|
+
resolver.mergeAndExit("M001", ctx);
|
|
451
|
+
|
|
452
|
+
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 1);
|
|
453
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
454
|
+
assert.equal(s.basePath, "/project"); // restored
|
|
455
|
+
assert.ok(ctx.messages.some((m) => m.msg.includes("no roadmap for merge")));
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("mergeAndExit in worktree mode restores to project root on merge failure", () => {
|
|
459
|
+
const s = makeSession({
|
|
460
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
461
|
+
originalBasePath: "/project",
|
|
462
|
+
});
|
|
463
|
+
const deps = makeDeps({
|
|
464
|
+
isInAutoWorktree: () => true,
|
|
465
|
+
getIsolationMode: () => "worktree",
|
|
466
|
+
mergeMilestoneToMain: () => {
|
|
467
|
+
throw new Error("conflict in main");
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
const ctx = makeNotifyCtx();
|
|
471
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
472
|
+
|
|
473
|
+
resolver.mergeAndExit("M001", ctx);
|
|
474
|
+
|
|
475
|
+
assert.equal(s.basePath, "/project"); // error recovery — restored
|
|
476
|
+
assert.ok(
|
|
477
|
+
ctx.messages.some(
|
|
478
|
+
(m) => m.level === "warning" && m.msg.includes("conflict in main"),
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1); // rebuilt after recovery
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ─── mergeAndExit Tests (branch mode) ────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
test("mergeAndExit in branch mode merges when on milestone branch", () => {
|
|
487
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
488
|
+
const deps = makeDeps({
|
|
489
|
+
isInAutoWorktree: () => false,
|
|
490
|
+
getIsolationMode: () => "branch",
|
|
491
|
+
getCurrentBranch: () => "milestone/M001",
|
|
492
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
493
|
+
});
|
|
494
|
+
const ctx = makeNotifyCtx();
|
|
495
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
496
|
+
|
|
497
|
+
resolver.mergeAndExit("M001", ctx);
|
|
498
|
+
|
|
499
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
|
|
500
|
+
assert.ok(ctx.messages.some((m) => m.msg.includes("branch mode")));
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("mergeAndExit in branch mode skips when not on milestone branch", () => {
|
|
504
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
505
|
+
const deps = makeDeps({
|
|
506
|
+
isInAutoWorktree: () => false,
|
|
507
|
+
getIsolationMode: () => "branch",
|
|
508
|
+
getCurrentBranch: () => "main",
|
|
509
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
510
|
+
});
|
|
511
|
+
const ctx = makeNotifyCtx();
|
|
512
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
513
|
+
|
|
514
|
+
resolver.mergeAndExit("M001", ctx);
|
|
515
|
+
|
|
516
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
517
|
+
assert.equal(ctx.messages.length, 0);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("mergeAndExit in branch mode handles merge failure gracefully", () => {
|
|
521
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
522
|
+
const deps = makeDeps({
|
|
523
|
+
isInAutoWorktree: () => false,
|
|
524
|
+
getIsolationMode: () => "branch",
|
|
525
|
+
getCurrentBranch: () => "milestone/M001",
|
|
526
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
527
|
+
mergeMilestoneToMain: () => {
|
|
528
|
+
throw new Error("branch merge conflict");
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
const ctx = makeNotifyCtx();
|
|
532
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
533
|
+
|
|
534
|
+
resolver.mergeAndExit("M001", ctx);
|
|
535
|
+
|
|
536
|
+
assert.ok(
|
|
537
|
+
ctx.messages.some(
|
|
538
|
+
(m) => m.level === "warning" && m.msg.includes("branch merge conflict"),
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("mergeAndExit in branch mode skips when no roadmap", () => {
|
|
544
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
545
|
+
const deps = makeDeps({
|
|
546
|
+
isInAutoWorktree: () => false,
|
|
547
|
+
getIsolationMode: () => "branch",
|
|
548
|
+
getCurrentBranch: () => "milestone/M001",
|
|
549
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
550
|
+
resolveMilestoneFile: () => null,
|
|
551
|
+
});
|
|
552
|
+
const ctx = makeNotifyCtx();
|
|
553
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
554
|
+
|
|
555
|
+
resolver.mergeAndExit("M001", ctx);
|
|
556
|
+
|
|
557
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("mergeAndExit in branch mode rebuilds GitService after merge", () => {
|
|
561
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
562
|
+
const deps = makeDeps({
|
|
563
|
+
isInAutoWorktree: () => false,
|
|
564
|
+
getIsolationMode: () => "branch",
|
|
565
|
+
getCurrentBranch: () => "milestone/M001",
|
|
566
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
567
|
+
});
|
|
568
|
+
const ctx = makeNotifyCtx();
|
|
569
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
570
|
+
|
|
571
|
+
resolver.mergeAndExit("M001", ctx);
|
|
572
|
+
|
|
573
|
+
assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// ─── mergeAndExit Tests (none mode) ──────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
test("mergeAndExit in none mode is a no-op", () => {
|
|
579
|
+
const s = makeSession();
|
|
580
|
+
const deps = makeDeps({
|
|
581
|
+
getIsolationMode: () => "none",
|
|
582
|
+
});
|
|
583
|
+
const ctx = makeNotifyCtx();
|
|
584
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
585
|
+
|
|
586
|
+
resolver.mergeAndExit("M001", ctx);
|
|
587
|
+
|
|
588
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
589
|
+
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
|
|
590
|
+
assert.equal(ctx.messages.length, 0);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// ─── mergeAndEnterNext Tests ─────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
test("mergeAndEnterNext calls mergeAndExit then enterMilestone", () => {
|
|
596
|
+
const s = makeSession({
|
|
597
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
598
|
+
originalBasePath: "/project",
|
|
599
|
+
});
|
|
600
|
+
const callOrder: string[] = [];
|
|
601
|
+
const deps = makeDeps({
|
|
602
|
+
isInAutoWorktree: () => true,
|
|
603
|
+
getIsolationMode: () => "worktree",
|
|
604
|
+
shouldUseWorktreeIsolation: () => true,
|
|
605
|
+
mergeMilestoneToMain: (
|
|
606
|
+
basePath: string,
|
|
607
|
+
milestoneId: string,
|
|
608
|
+
_roadmap: string,
|
|
609
|
+
) => {
|
|
610
|
+
callOrder.push(`merge:${milestoneId}`);
|
|
611
|
+
return { pushed: false };
|
|
612
|
+
},
|
|
613
|
+
getAutoWorktreePath: () => null,
|
|
614
|
+
createAutoWorktree: (basePath: string, milestoneId: string) => {
|
|
615
|
+
callOrder.push(`create:${milestoneId}`);
|
|
616
|
+
return `/project/.gsd/worktrees/${milestoneId}`;
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
const ctx = makeNotifyCtx();
|
|
620
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
621
|
+
|
|
622
|
+
resolver.mergeAndEnterNext("M001", "M002", ctx);
|
|
623
|
+
|
|
624
|
+
assert.deepEqual(callOrder, ["merge:M001", "create:M002"]);
|
|
625
|
+
assert.equal(s.basePath, "/project/.gsd/worktrees/M002");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("mergeAndEnterNext enters next milestone even if merge fails", () => {
|
|
629
|
+
const s = makeSession({
|
|
630
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
631
|
+
originalBasePath: "/project",
|
|
632
|
+
});
|
|
633
|
+
const deps = makeDeps({
|
|
634
|
+
isInAutoWorktree: (basePath: string) => basePath.includes("worktrees"),
|
|
635
|
+
getIsolationMode: () => "worktree",
|
|
636
|
+
shouldUseWorktreeIsolation: () => true,
|
|
637
|
+
mergeMilestoneToMain: () => {
|
|
638
|
+
throw new Error("merge failed");
|
|
639
|
+
},
|
|
640
|
+
getAutoWorktreePath: () => null,
|
|
641
|
+
createAutoWorktree: (_basePath: string, milestoneId: string) => {
|
|
642
|
+
return `/project/.gsd/worktrees/${milestoneId}`;
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
const ctx = makeNotifyCtx();
|
|
646
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
647
|
+
|
|
648
|
+
resolver.mergeAndEnterNext("M001", "M002", ctx);
|
|
649
|
+
|
|
650
|
+
// Merge failed but enter should still happen
|
|
651
|
+
assert.equal(s.basePath, "/project/.gsd/worktrees/M002");
|
|
652
|
+
assert.ok(
|
|
653
|
+
ctx.messages.some(
|
|
654
|
+
(m) => m.level === "warning" && m.msg.includes("merge failed"),
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
assert.ok(
|
|
658
|
+
ctx.messages.some(
|
|
659
|
+
(m) => m.level === "info" && m.msg.includes("Entered worktree"),
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// ─── GitService Rebuild Atomicity ────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
test("GitService is rebuilt with the NEW basePath after enterMilestone", () => {
|
|
667
|
+
const s = makeSession();
|
|
668
|
+
let gitServiceBasePath = "";
|
|
669
|
+
const deps = makeDeps({
|
|
670
|
+
getAutoWorktreePath: () => null,
|
|
671
|
+
GitServiceImpl: class {
|
|
672
|
+
constructor(basePath: string, _config: unknown) {
|
|
673
|
+
gitServiceBasePath = basePath;
|
|
674
|
+
}
|
|
675
|
+
} as unknown as WorktreeResolverDeps["GitServiceImpl"],
|
|
676
|
+
});
|
|
677
|
+
const ctx = makeNotifyCtx();
|
|
678
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
679
|
+
|
|
680
|
+
resolver.enterMilestone("M001", ctx);
|
|
681
|
+
|
|
682
|
+
assert.equal(gitServiceBasePath, "/project/.gsd/worktrees/M001"); // new path, not old
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("GitService is rebuilt with originalBasePath after exitMilestone", () => {
|
|
686
|
+
const s = makeSession({
|
|
687
|
+
basePath: "/project/.gsd/worktrees/M001",
|
|
688
|
+
originalBasePath: "/project",
|
|
689
|
+
});
|
|
690
|
+
let gitServiceBasePath = "";
|
|
691
|
+
const deps = makeDeps({
|
|
692
|
+
isInAutoWorktree: () => true,
|
|
693
|
+
GitServiceImpl: class {
|
|
694
|
+
constructor(basePath: string, _config: unknown) {
|
|
695
|
+
gitServiceBasePath = basePath;
|
|
696
|
+
}
|
|
697
|
+
} as unknown as WorktreeResolverDeps["GitServiceImpl"],
|
|
698
|
+
});
|
|
699
|
+
const ctx = makeNotifyCtx();
|
|
700
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
701
|
+
|
|
702
|
+
resolver.exitMilestone("M001", ctx);
|
|
703
|
+
|
|
704
|
+
assert.equal(gitServiceBasePath, "/project"); // project root, not worktree
|
|
705
|
+
});
|