gsd-pi 2.13.0 → 2.14.0
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 +3 -3
- package/dist/cli.js +1 -0
- package/dist/loader.js +50 -6
- package/dist/resource-loader.d.ts +7 -6
- package/dist/resource-loader.js +15 -8
- package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/dist/resources/extensions/gsd/auto.ts +252 -370
- package/dist/resources/extensions/gsd/commands.ts +118 -34
- package/dist/resources/extensions/gsd/doctor.ts +29 -4
- package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/dist/resources/extensions/gsd/git-service.ts +8 -431
- package/dist/resources/extensions/gsd/gitignore.ts +11 -4
- package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
- package/dist/resources/extensions/gsd/preferences.ts +18 -17
- package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
- package/dist/resources/extensions/gsd/state.ts +26 -8
- package/dist/resources/extensions/gsd/templates/state.md +0 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/dist/resources/extensions/gsd/types.ts +0 -1
- package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/dist/resources/extensions/gsd/worktree.ts +7 -65
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +12 -4
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +10 -2
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/src/providers/google.ts +20 -8
- package/packages/pi-ai/src/providers/mistral.ts +14 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
- package/packages/pi-tui/dist/components/input.d.ts +1 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +10 -0
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/input.ts +11 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/src/resources/extensions/gsd/auto.ts +252 -370
- package/src/resources/extensions/gsd/commands.ts +118 -34
- package/src/resources/extensions/gsd/doctor.ts +29 -4
- package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/src/resources/extensions/gsd/git-service.ts +8 -431
- package/src/resources/extensions/gsd/gitignore.ts +11 -4
- package/src/resources/extensions/gsd/guided-flow.ts +141 -5
- package/src/resources/extensions/gsd/preferences.ts +18 -17
- package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/src/resources/extensions/gsd/prompts/queue.md +7 -1
- package/src/resources/extensions/gsd/state.ts +26 -8
- package/src/resources/extensions/gsd/templates/state.md +0 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/src/resources/extensions/gsd/worktree.ts +7 -65
- package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* Tests the full lifecycle of GSD operations inside a worktree:
|
|
5
5
|
* - Branch namespacing (gsd/<wt>/<M>/<S> instead of gsd/<M>/<S>)
|
|
6
6
|
* - getMainBranch returns worktree/<name> inside a worktree
|
|
7
|
-
* - switchToMain goes to worktree/<name>, not main
|
|
8
|
-
* - mergeSliceToMain merges into worktree/<name>
|
|
9
7
|
* - Parallel worktrees don't conflict on branch names
|
|
10
8
|
* - State derivation works correctly inside worktrees
|
|
11
9
|
*/
|
|
@@ -19,21 +17,15 @@ import {
|
|
|
19
17
|
createWorktree,
|
|
20
18
|
listWorktrees,
|
|
21
19
|
removeWorktree,
|
|
22
|
-
worktreePath,
|
|
23
|
-
worktreeBranchName,
|
|
24
20
|
} from "../worktree-manager.ts";
|
|
25
21
|
|
|
26
22
|
import {
|
|
27
23
|
detectWorktreeName,
|
|
28
|
-
ensureSliceBranch,
|
|
29
|
-
getActiveSliceBranch,
|
|
30
24
|
getCurrentBranch,
|
|
31
25
|
getMainBranch,
|
|
32
26
|
getSliceBranchName,
|
|
33
|
-
isOnSliceBranch,
|
|
34
|
-
mergeSliceToMain,
|
|
35
|
-
switchToMain,
|
|
36
27
|
autoCommitCurrentBranch,
|
|
28
|
+
SLICE_BRANCH_RE,
|
|
37
29
|
} from "../worktree.ts";
|
|
38
30
|
|
|
39
31
|
import { deriveState } from "../state.ts";
|
|
@@ -104,21 +96,20 @@ async function main(): Promise<void> {
|
|
|
104
96
|
console.log("\n=== Worktree initial branch ===");
|
|
105
97
|
assertEq(getCurrentBranch(wt.path), "worktree/alpha", "worktree starts on its own branch");
|
|
106
98
|
|
|
107
|
-
// ── ensureSliceBranch inside worktree ──────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
console.log("\n=== ensureSliceBranch in worktree ===");
|
|
110
|
-
const created = ensureSliceBranch(wt.path, "M001", "S01");
|
|
111
|
-
assertTrue(created, "slice branch created");
|
|
112
|
-
assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch");
|
|
113
|
-
assertTrue(isOnSliceBranch(wt.path), "isOnSliceBranch returns true");
|
|
114
|
-
assertEq(getActiveSliceBranch(wt.path), "gsd/alpha/M001/S01", "getActiveSliceBranch returns namespaced branch");
|
|
115
|
-
|
|
116
99
|
// ── Verify branch name helper ──────────────────────────────────────────────
|
|
117
100
|
|
|
118
101
|
console.log("\n=== getSliceBranchName with worktree ===");
|
|
119
102
|
assertEq(getSliceBranchName("M001", "S01", "alpha"), "gsd/alpha/M001/S01", "explicit worktree param");
|
|
120
103
|
assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "no worktree param = plain branch");
|
|
121
104
|
|
|
105
|
+
// ── Slice branch creation and detection inside worktree ────────────────────
|
|
106
|
+
|
|
107
|
+
console.log("\n=== Slice branch in worktree ===");
|
|
108
|
+
const sliceBranch = getSliceBranchName("M001", "S01", "alpha");
|
|
109
|
+
run(`git checkout -b ${sliceBranch}`, wt.path);
|
|
110
|
+
assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch");
|
|
111
|
+
assertTrue(SLICE_BRANCH_RE.test(getCurrentBranch(wt.path)), "slice branch regex matches namespaced branch");
|
|
112
|
+
|
|
122
113
|
// ── Do work on slice branch, then merge to worktree branch ─────────────────
|
|
123
114
|
|
|
124
115
|
console.log("\n=== Work and merge slice in worktree ===");
|
|
@@ -126,14 +117,12 @@ async function main(): Promise<void> {
|
|
|
126
117
|
run("git add .", wt.path);
|
|
127
118
|
run('git commit -m "feat: add feature"', wt.path);
|
|
128
119
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
assertEq(getCurrentBranch(wt.path), "worktree/alpha", "
|
|
120
|
+
// Checkout worktree base branch and merge slice branch
|
|
121
|
+
run("git checkout worktree/alpha", wt.path);
|
|
122
|
+
assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch");
|
|
132
123
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assertEq(merge.branch, "gsd/alpha/M001/S01", "merged the namespaced branch");
|
|
136
|
-
assertTrue(merge.deletedBranch, "slice branch deleted after merge");
|
|
124
|
+
run(`git merge --no-ff ${sliceBranch} -m "feat(M001/S01): First"`, wt.path);
|
|
125
|
+
run(`git branch -d ${sliceBranch}`, wt.path);
|
|
137
126
|
assertEq(getCurrentBranch(wt.path), "worktree/alpha", "still on worktree branch after merge");
|
|
138
127
|
assertTrue(readFileSync(join(wt.path, "feature.txt"), "utf-8").includes("new feature"), "merge brought feature to worktree branch");
|
|
139
128
|
|
|
@@ -144,36 +133,19 @@ async function main(): Promise<void> {
|
|
|
144
133
|
// ── Second slice in same worktree ──────────────────────────────────────────
|
|
145
134
|
|
|
146
135
|
console.log("\n=== Second slice in worktree ===");
|
|
147
|
-
const
|
|
148
|
-
|
|
136
|
+
const sliceBranch2 = getSliceBranchName("M001", "S02", "alpha");
|
|
137
|
+
run(`git checkout -b ${sliceBranch2}`, wt.path);
|
|
149
138
|
assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S02", "on S02 namespaced branch");
|
|
150
139
|
|
|
151
140
|
writeFileSync(join(wt.path, "feature2.txt"), "second feature\n", "utf-8");
|
|
152
141
|
run("git add .", wt.path);
|
|
153
142
|
run('git commit -m "feat: add feature 2"', wt.path);
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
144
|
+
run("git checkout worktree/alpha", wt.path);
|
|
145
|
+
run(`git merge --no-ff ${sliceBranch2} -m "feat(M001/S02): Second"`, wt.path);
|
|
146
|
+
run(`git branch -d ${sliceBranch2}`, wt.path);
|
|
158
147
|
assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch");
|
|
159
148
|
|
|
160
|
-
// ── Main tree can still do its own slice work independently ────────────────
|
|
161
|
-
|
|
162
|
-
console.log("\n=== Main tree independent slice work ===");
|
|
163
|
-
assertEq(getCurrentBranch(base), "main", "main tree still on main");
|
|
164
|
-
const mainCreated = ensureSliceBranch(base, "M001", "S01");
|
|
165
|
-
assertTrue(mainCreated, "main tree can create S01 branch (no conflict with worktree)");
|
|
166
|
-
assertEq(getCurrentBranch(base), "gsd/M001/S01", "main tree on plain branch name");
|
|
167
|
-
|
|
168
|
-
writeFileSync(join(base, "main-feature.txt"), "main work\n", "utf-8");
|
|
169
|
-
run("git add .", base);
|
|
170
|
-
run('git commit -m "feat: main work"', base);
|
|
171
|
-
|
|
172
|
-
switchToMain(base);
|
|
173
|
-
assertEq(getCurrentBranch(base), "main", "main tree switchToMain goes to main");
|
|
174
|
-
const mainMerge = mergeSliceToMain(base, "M001", "S01", "First");
|
|
175
|
-
assertEq(mainMerge.branch, "gsd/M001/S01", "main tree merge uses plain branch");
|
|
176
|
-
|
|
177
149
|
// ── Parallel worktrees don't conflict ──────────────────────────────────────
|
|
178
150
|
|
|
179
151
|
console.log("\n=== Parallel worktrees ===");
|
|
@@ -181,13 +153,13 @@ async function main(): Promise<void> {
|
|
|
181
153
|
assertEq(getMainBranch(wt2.path), "worktree/beta", "second worktree has its own base branch");
|
|
182
154
|
|
|
183
155
|
// Both worktrees can create S01 branches without conflict
|
|
184
|
-
const
|
|
185
|
-
|
|
156
|
+
const betaBranch = getSliceBranchName("M001", "S01", "beta");
|
|
157
|
+
run(`git checkout -b ${betaBranch}`, wt2.path);
|
|
186
158
|
assertEq(getCurrentBranch(wt2.path), "gsd/beta/M001/S01", "beta has its own namespaced branch");
|
|
187
159
|
|
|
188
160
|
// Alpha worktree can re-create S01 too (it was already merged+deleted earlier)
|
|
189
|
-
const
|
|
190
|
-
|
|
161
|
+
const alphaReBranch = getSliceBranchName("M001", "S01", "alpha");
|
|
162
|
+
run(`git checkout -b ${alphaReBranch}`, wt.path);
|
|
191
163
|
assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "alpha re-created S01");
|
|
192
164
|
|
|
193
165
|
// Both exist simultaneously
|
|
@@ -199,7 +171,7 @@ async function main(): Promise<void> {
|
|
|
199
171
|
|
|
200
172
|
console.log("\n=== State derivation in worktree ===");
|
|
201
173
|
// Switch alpha back to its base so deriveState sees milestone files
|
|
202
|
-
|
|
174
|
+
run("git checkout worktree/alpha", wt.path);
|
|
203
175
|
const state = await deriveState(wt.path);
|
|
204
176
|
assertTrue(state.activeMilestone !== null, "worktree has active milestone");
|
|
205
177
|
assertEq(state.activeMilestone?.id, "M001", "correct milestone");
|
|
@@ -207,7 +179,8 @@ async function main(): Promise<void> {
|
|
|
207
179
|
// ── autoCommitCurrentBranch in worktree ────────────────────────────────────
|
|
208
180
|
|
|
209
181
|
console.log("\n=== autoCommitCurrentBranch in worktree ===");
|
|
210
|
-
|
|
182
|
+
// Re-checkout the beta slice branch
|
|
183
|
+
run(`git checkout ${betaBranch}`, wt2.path);
|
|
211
184
|
writeFileSync(join(wt2.path, "dirty.txt"), "uncommitted\n", "utf-8");
|
|
212
185
|
const commitMsg = autoCommitCurrentBranch(wt2.path, "execute-task", "M001/S01/T01");
|
|
213
186
|
assertTrue(commitMsg !== null, "auto-commit works in worktree");
|
|
@@ -217,8 +190,8 @@ async function main(): Promise<void> {
|
|
|
217
190
|
|
|
218
191
|
console.log("\n=== Cleanup ===");
|
|
219
192
|
// Switch worktrees back to their base branches before removal
|
|
220
|
-
|
|
221
|
-
|
|
193
|
+
run("git checkout worktree/alpha", wt.path);
|
|
194
|
+
run("git checkout worktree/beta", wt2.path);
|
|
222
195
|
removeWorktree(base, "alpha", { deleteBranch: true });
|
|
223
196
|
removeWorktree(base, "beta", { deleteBranch: true });
|
|
224
197
|
assertEq(listWorktrees(base).length, 0, "all worktrees removed");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
@@ -7,21 +7,14 @@ import {
|
|
|
7
7
|
autoCommitCurrentBranch,
|
|
8
8
|
captureIntegrationBranch,
|
|
9
9
|
detectWorktreeName,
|
|
10
|
-
ensureSliceBranch,
|
|
11
|
-
getActiveSliceBranch,
|
|
12
10
|
getCurrentBranch,
|
|
13
11
|
getMainBranch,
|
|
14
12
|
getSliceBranchName,
|
|
15
|
-
isOnSliceBranch,
|
|
16
|
-
mergeSliceToMain,
|
|
17
13
|
parseSliceBranch,
|
|
18
14
|
setActiveMilestoneId,
|
|
19
15
|
SLICE_BRANCH_RE,
|
|
20
|
-
switchToMain,
|
|
21
16
|
} from "../worktree.ts";
|
|
22
17
|
import { readIntegrationBranch } from "../git-service.ts";
|
|
23
|
-
import { deriveState } from "../state.ts";
|
|
24
|
-
import { indexWorkspace } from "../workspace-index.ts";
|
|
25
18
|
import { createTestContext } from './test-helpers.ts';
|
|
26
19
|
|
|
27
20
|
const { assertEq, assertTrue, report } = createTestContext();
|
|
@@ -41,27 +34,6 @@ run("git add .", base);
|
|
|
41
34
|
run('git commit -m "chore: init"', base);
|
|
42
35
|
|
|
43
36
|
async function main(): Promise<void> {
|
|
44
|
-
console.log("\n=== ensureSliceBranch ===");
|
|
45
|
-
const created = ensureSliceBranch(base, "M001", "S01");
|
|
46
|
-
assertTrue(created, "branch created on first ensure");
|
|
47
|
-
assertEq(getCurrentBranch(base), "gsd/M001/S01", "switched to slice branch");
|
|
48
|
-
|
|
49
|
-
console.log("\n=== idempotent ensure ===");
|
|
50
|
-
const secondCreate = ensureSliceBranch(base, "M001", "S01");
|
|
51
|
-
assertEq(secondCreate, false, "branch not recreated on second ensure");
|
|
52
|
-
assertEq(getCurrentBranch(base), "gsd/M001/S01", "still on slice branch");
|
|
53
|
-
|
|
54
|
-
console.log("\n=== getActiveSliceBranch ===");
|
|
55
|
-
assertEq(getActiveSliceBranch(base), "gsd/M001/S01", "getActiveSliceBranch returns current slice branch");
|
|
56
|
-
|
|
57
|
-
console.log("\n=== state surfaces active branch ===");
|
|
58
|
-
const state = await deriveState(base);
|
|
59
|
-
assertEq(state.activeBranch, "gsd/M001/S01", "state exposes active branch");
|
|
60
|
-
|
|
61
|
-
console.log("\n=== workspace index surfaces branch ===");
|
|
62
|
-
const index = await indexWorkspace(base);
|
|
63
|
-
const slice = index.milestones[0]?.slices[0];
|
|
64
|
-
assertEq(slice?.branch, "gsd/M001/S01", "workspace index exposes branch");
|
|
65
37
|
|
|
66
38
|
console.log("\n=== autoCommitCurrentBranch ===");
|
|
67
39
|
// Clean — should return null
|
|
@@ -75,56 +47,6 @@ async function main(): Promise<void> {
|
|
|
75
47
|
assertTrue(dirtyResult!.includes("M001/S01/T01"), "commit message includes unit id");
|
|
76
48
|
assertEq(run("git status --short", base), "", "repo is clean after auto-commit");
|
|
77
49
|
|
|
78
|
-
console.log("\n=== switchToMain ===");
|
|
79
|
-
switchToMain(base);
|
|
80
|
-
assertEq(getCurrentBranch(base), "main", "switched back to main");
|
|
81
|
-
assertEq(getActiveSliceBranch(base), null, "getActiveSliceBranch returns null on main");
|
|
82
|
-
|
|
83
|
-
console.log("\n=== mergeSliceToMain ===");
|
|
84
|
-
// Switch back to slice, make a change, switch to main, merge
|
|
85
|
-
ensureSliceBranch(base, "M001", "S01");
|
|
86
|
-
writeFileSync(join(base, "README.md"), "hello from slice\n", "utf-8");
|
|
87
|
-
run("git add README.md", base);
|
|
88
|
-
run('git commit -m "feat: slice change"', base);
|
|
89
|
-
switchToMain(base);
|
|
90
|
-
|
|
91
|
-
const merge = mergeSliceToMain(base, "M001", "S01", "Slice One");
|
|
92
|
-
assertEq(merge.branch, "gsd/M001/S01", "merge reports branch");
|
|
93
|
-
assertEq(getCurrentBranch(base), "main", "still on main after merge");
|
|
94
|
-
assertTrue(readFileSync(join(base, "README.md"), "utf-8").includes("slice"), "main got squashed content");
|
|
95
|
-
assertTrue(merge.deletedBranch, "branch was deleted");
|
|
96
|
-
|
|
97
|
-
// Verify branch is actually gone
|
|
98
|
-
const branches = run("git branch", base);
|
|
99
|
-
assertTrue(!branches.includes("gsd/M001/S01"), "slice branch no longer exists");
|
|
100
|
-
|
|
101
|
-
console.log("\n=== switchToMain auto-commits dirty files ===");
|
|
102
|
-
// Set up S02
|
|
103
|
-
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
|
|
104
|
-
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
|
|
105
|
-
"# M001: Demo", "", "## Slices",
|
|
106
|
-
"- [x] **S01: Slice One** `risk:low` `depends:[]`", " > Done",
|
|
107
|
-
"- [ ] **S02: Slice Two** `risk:low` `depends:[]`", " > Demo 2",
|
|
108
|
-
].join("\n") + "\n", "utf-8");
|
|
109
|
-
run("git add .", base);
|
|
110
|
-
run('git commit -m "chore: add S02"', base);
|
|
111
|
-
|
|
112
|
-
ensureSliceBranch(base, "M001", "S02");
|
|
113
|
-
writeFileSync(join(base, "feature.txt"), "new feature\n", "utf-8");
|
|
114
|
-
// Don't commit — switchToMain should auto-commit
|
|
115
|
-
switchToMain(base);
|
|
116
|
-
assertEq(getCurrentBranch(base), "main", "switched to main despite dirty files");
|
|
117
|
-
|
|
118
|
-
// Verify the commit happened on the slice branch
|
|
119
|
-
ensureSliceBranch(base, "M001", "S02");
|
|
120
|
-
assertTrue(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "dirty file was committed on slice branch");
|
|
121
|
-
switchToMain(base);
|
|
122
|
-
|
|
123
|
-
// Now merge S02
|
|
124
|
-
const mergeS02 = mergeSliceToMain(base, "M001", "S02", "Slice Two");
|
|
125
|
-
assertTrue(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "main got feature from auto-committed branch");
|
|
126
|
-
assertEq(mergeS02.deletedBranch, true, "S02 branch deleted");
|
|
127
|
-
|
|
128
50
|
console.log("\n=== getSliceBranchName ===");
|
|
129
51
|
assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "branch name format correct");
|
|
130
52
|
assertEq(getSliceBranchName("M001", "S01", null), "gsd/M001/S01", "null worktree = plain branch");
|
|
@@ -161,90 +83,8 @@ async function main(): Promise<void> {
|
|
|
161
83
|
assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/feature-auth"), "feature-auth", "detects worktree name");
|
|
162
84
|
assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/my-wt/subdir"), "my-wt", "detects worktree with subdir");
|
|
163
85
|
|
|
164
|
-
// ── Regression: slice branch from non-main working branch ───────────
|
|
165
|
-
// Reproduces the bug where planning artifacts committed to a working
|
|
166
|
-
// branch (e.g. "developer") are lost when the slice branch is created
|
|
167
|
-
// from "main" which doesn't have them.
|
|
168
|
-
console.log("\n=== ensureSliceBranch from non-main working branch ===");
|
|
169
|
-
const base2 = mkdtempSync(join(tmpdir(), "gsd-branch-base-test-"));
|
|
170
|
-
run("git init -b main", base2);
|
|
171
|
-
run('git config user.name "Pi Test"', base2);
|
|
172
|
-
run('git config user.email "pi@example.com"', base2);
|
|
173
|
-
writeFileSync(join(base2, "README.md"), "hello\n", "utf-8");
|
|
174
|
-
run("git add .", base2);
|
|
175
|
-
run('git commit -m "chore: init"', base2);
|
|
176
|
-
|
|
177
|
-
// Create a "developer" branch with planning artifacts (like the real scenario)
|
|
178
|
-
run("git checkout -b developer", base2);
|
|
179
|
-
mkdirSync(join(base2, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
180
|
-
writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\nGoal: fix eslint\n", "utf-8");
|
|
181
|
-
writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
|
|
182
|
-
"# M001: ESLint Cleanup", "", "## Slices",
|
|
183
|
-
"- [ ] **S01: Config Fix** `risk:low` `depends:[]`", " > Fix config",
|
|
184
|
-
].join("\n") + "\n", "utf-8");
|
|
185
|
-
run("git add .", base2);
|
|
186
|
-
run('git commit -m "docs(M001): context and roadmap"', base2);
|
|
187
|
-
|
|
188
|
-
// Verify main does NOT have the artifacts
|
|
189
|
-
const mainRoadmap = run("git show main:.gsd/milestones/M001/M001-ROADMAP.md 2>&1 || echo MISSING", base2);
|
|
190
|
-
assertTrue(mainRoadmap.includes("MISSING") || mainRoadmap.includes("does not exist"), "main branch lacks roadmap");
|
|
191
|
-
|
|
192
|
-
// Now create slice branch from developer — should inherit artifacts
|
|
193
|
-
assertEq(getCurrentBranch(base2), "developer", "on developer branch before ensure");
|
|
194
|
-
const created3 = ensureSliceBranch(base2, "M001", "S01");
|
|
195
|
-
assertTrue(created3, "slice branch created from developer");
|
|
196
|
-
assertEq(getCurrentBranch(base2), "gsd/M001/S01", "switched to slice branch");
|
|
197
|
-
|
|
198
|
-
// The critical assertion: planning artifacts must exist on the slice branch
|
|
199
|
-
assertTrue(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), "roadmap exists on slice branch");
|
|
200
|
-
assertTrue(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md")), "context exists on slice branch");
|
|
201
|
-
|
|
202
|
-
// Verify deriveState sees the correct phase (not pre-planning)
|
|
203
|
-
const state2 = await deriveState(base2);
|
|
204
|
-
assertEq(state2.phase, "planning", "deriveState sees planning phase on slice branch");
|
|
205
|
-
assertTrue(state2.activeSlice !== null, "active slice found");
|
|
206
|
-
assertEq(state2.activeSlice!.id, "S01", "active slice is S01");
|
|
207
|
-
|
|
208
|
-
rmSync(base2, { recursive: true, force: true });
|
|
209
|
-
|
|
210
|
-
// ── Slice branch from another slice branch falls back to main ───────
|
|
211
|
-
console.log("\n=== ensureSliceBranch from slice branch falls back to main ===");
|
|
212
|
-
const base3 = mkdtempSync(join(tmpdir(), "gsd-branch-chain-test-"));
|
|
213
|
-
run("git init -b main", base3);
|
|
214
|
-
run('git config user.name "Pi Test"', base3);
|
|
215
|
-
run('git config user.email "pi@example.com"', base3);
|
|
216
|
-
mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
217
|
-
mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
|
|
218
|
-
writeFileSync(join(base3, "README.md"), "hello\n", "utf-8");
|
|
219
|
-
writeFileSync(join(base3, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
|
|
220
|
-
"# M001: Demo", "", "## Slices",
|
|
221
|
-
"- [ ] **S01: First** `risk:low` `depends:[]`", " > first",
|
|
222
|
-
"- [ ] **S02: Second** `risk:low` `depends:[]`", " > second",
|
|
223
|
-
].join("\n") + "\n", "utf-8");
|
|
224
|
-
run("git add .", base3);
|
|
225
|
-
run('git commit -m "chore: init"', base3);
|
|
226
|
-
|
|
227
|
-
ensureSliceBranch(base3, "M001", "S01");
|
|
228
|
-
assertEq(getCurrentBranch(base3), "gsd/M001/S01", "on S01 slice branch");
|
|
229
|
-
|
|
230
|
-
// Creating S02 while on S01 should NOT chain from S01 — should use main
|
|
231
|
-
const created4 = ensureSliceBranch(base3, "M001", "S02");
|
|
232
|
-
assertTrue(created4, "S02 branch created");
|
|
233
|
-
assertEq(getCurrentBranch(base3), "gsd/M001/S02", "switched to S02");
|
|
234
|
-
|
|
235
|
-
// S02 should be based on main, not on gsd/M001/S01
|
|
236
|
-
const s02Base = run("git merge-base main gsd/M001/S02", base3);
|
|
237
|
-
const mainHead = run("git rev-parse main", base3);
|
|
238
|
-
assertEq(s02Base, mainHead, "S02 is based on main, not on S01 slice branch");
|
|
239
|
-
|
|
240
|
-
rmSync(base3, { recursive: true, force: true });
|
|
241
|
-
|
|
242
86
|
// ═══════════════════════════════════════════════════════════════════════
|
|
243
87
|
// Integration branch — facade-level tests
|
|
244
|
-
//
|
|
245
|
-
// These exercise the same codepath auto.ts uses:
|
|
246
|
-
// captureIntegrationBranch() → setActiveMilestoneId() → getMainBranch()
|
|
247
|
-
// → switchToMain() → mergeSliceToMain()
|
|
248
88
|
// ═══════════════════════════════════════════════════════════════════════
|
|
249
89
|
|
|
250
90
|
// ── captureIntegrationBranch on a feature branch ──────────────────────
|
|
@@ -273,43 +113,6 @@ async function main(): Promise<void> {
|
|
|
273
113
|
rmSync(repo, { recursive: true, force: true });
|
|
274
114
|
}
|
|
275
115
|
|
|
276
|
-
// ── captureIntegrationBranch is idempotent on same lineage ──────────
|
|
277
|
-
|
|
278
|
-
console.log("\n=== captureIntegrationBranch: idempotent ===");
|
|
279
|
-
|
|
280
|
-
{
|
|
281
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-integ-idem-"));
|
|
282
|
-
run("git init -b main", repo);
|
|
283
|
-
run("git config user.name 'Pi Test'", repo);
|
|
284
|
-
run("git config user.email 'pi@example.com'", repo);
|
|
285
|
-
writeFileSync(join(repo, "README.md"), "init\n");
|
|
286
|
-
run("git add -A && git commit -m init", repo);
|
|
287
|
-
run("git checkout -b f-first", repo);
|
|
288
|
-
|
|
289
|
-
captureIntegrationBranch(repo, "M001");
|
|
290
|
-
setActiveMilestoneId(repo, "M001");
|
|
291
|
-
assertEq(readIntegrationBranch(repo, "M001"), "f-first",
|
|
292
|
-
"first capture records f-first");
|
|
293
|
-
|
|
294
|
-
// Capture again on the same branch (simulates restart/resume) — should NOT overwrite
|
|
295
|
-
captureIntegrationBranch(repo, "M001");
|
|
296
|
-
assertEq(readIntegrationBranch(repo, "M001"), "f-first",
|
|
297
|
-
"second capture on same branch does not overwrite");
|
|
298
|
-
|
|
299
|
-
// After creating a slice branch (which inherits the metadata commit),
|
|
300
|
-
// capture should still be idempotent
|
|
301
|
-
ensureSliceBranch(repo, "M001", "S01");
|
|
302
|
-
// Now on gsd/M001/S01 — capture should be no-op (slice branch rejected)
|
|
303
|
-
captureIntegrationBranch(repo, "M001");
|
|
304
|
-
switchToMain(repo);
|
|
305
|
-
assertEq(readIntegrationBranch(repo, "M001"), "f-first",
|
|
306
|
-
"capture from slice branch is no-op, original preserved");
|
|
307
|
-
assertEq(getCurrentBranch(repo), "f-first",
|
|
308
|
-
"switchToMain returns to feature branch, confirming integration branch works");
|
|
309
|
-
|
|
310
|
-
rmSync(repo, { recursive: true, force: true });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
116
|
// ── captureIntegrationBranch skips slice branches ─────────────────────
|
|
314
117
|
|
|
315
118
|
console.log("\n=== captureIntegrationBranch: skips slice branches ===");
|
|
@@ -359,234 +162,6 @@ async function main(): Promise<void> {
|
|
|
359
162
|
rmSync(repo, { recursive: true, force: true });
|
|
360
163
|
}
|
|
361
164
|
|
|
362
|
-
// ── Full multi-slice lifecycle on a feature branch ────────────────────
|
|
363
|
-
//
|
|
364
|
-
// Simulates what auto.ts does: start on feature branch, capture it,
|
|
365
|
-
// create S01, work, merge S01 back to feature branch, then S02 branches
|
|
366
|
-
// from feature branch (not main), works, merges to feature branch.
|
|
367
|
-
// Main stays untouched throughout.
|
|
368
|
-
|
|
369
|
-
console.log("\n=== Multi-slice lifecycle on feature branch ===");
|
|
370
|
-
|
|
371
|
-
{
|
|
372
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-integ-multi-"));
|
|
373
|
-
run("git init -b main", repo);
|
|
374
|
-
run("git config user.name 'Pi Test'", repo);
|
|
375
|
-
run("git config user.email 'pi@example.com'", repo);
|
|
376
|
-
writeFileSync(join(repo, "README.md"), "base\n");
|
|
377
|
-
run("git add -A && git commit -m init", repo);
|
|
378
|
-
|
|
379
|
-
// User creates feature branch
|
|
380
|
-
run("git checkout -b feature/big-change", repo);
|
|
381
|
-
writeFileSync(join(repo, "setup.txt"), "feature setup\n");
|
|
382
|
-
run('git add -A && git commit -m "feat: initial setup"', repo);
|
|
383
|
-
|
|
384
|
-
// auto.ts startup: capture + set milestone
|
|
385
|
-
captureIntegrationBranch(repo, "M001");
|
|
386
|
-
setActiveMilestoneId(repo, "M001");
|
|
387
|
-
|
|
388
|
-
assertEq(getMainBranch(repo), "feature/big-change",
|
|
389
|
-
"multi: getMainBranch returns feature branch");
|
|
390
|
-
|
|
391
|
-
// ── S01 lifecycle ──────────────────────────────────────────────────
|
|
392
|
-
ensureSliceBranch(repo, "M001", "S01");
|
|
393
|
-
assertEq(getCurrentBranch(repo), "gsd/M001/S01", "multi: on S01");
|
|
394
|
-
|
|
395
|
-
// Verify S01 has feature branch content
|
|
396
|
-
assertTrue(existsSync(join(repo, "setup.txt")),
|
|
397
|
-
"multi: S01 inherited feature branch content");
|
|
398
|
-
|
|
399
|
-
writeFileSync(join(repo, "s01-work.txt"), "s01 output\n");
|
|
400
|
-
run('git add -A && git commit -m "feat(S01): work"', repo);
|
|
401
|
-
|
|
402
|
-
switchToMain(repo);
|
|
403
|
-
assertEq(getCurrentBranch(repo), "feature/big-change",
|
|
404
|
-
"multi: switchToMain goes to feature branch");
|
|
405
|
-
|
|
406
|
-
const s01merge = mergeSliceToMain(repo, "M001", "S01", "First slice");
|
|
407
|
-
assertEq(getCurrentBranch(repo), "feature/big-change",
|
|
408
|
-
"multi: after S01 merge, on feature branch");
|
|
409
|
-
assertTrue(existsSync(join(repo, "s01-work.txt")),
|
|
410
|
-
"multi: S01 work merged to feature branch");
|
|
411
|
-
assertTrue(s01merge.deletedBranch, "multi: S01 branch deleted");
|
|
412
|
-
|
|
413
|
-
// Main should NOT have S01 work
|
|
414
|
-
run("git stash", repo); // stash any .gsd changes
|
|
415
|
-
run("git checkout main", repo);
|
|
416
|
-
assertTrue(!existsSync(join(repo, "s01-work.txt")),
|
|
417
|
-
"multi: main does NOT have S01 work");
|
|
418
|
-
run("git checkout feature/big-change", repo);
|
|
419
|
-
run("git stash pop || true", repo);
|
|
420
|
-
|
|
421
|
-
// ── S02 lifecycle ──────────────────────────────────────────────────
|
|
422
|
-
// S02 should branch from feature/big-change which now has S01's work
|
|
423
|
-
ensureSliceBranch(repo, "M001", "S02");
|
|
424
|
-
assertEq(getCurrentBranch(repo), "gsd/M001/S02", "multi: on S02");
|
|
425
|
-
|
|
426
|
-
// S02 should have S01's merged output (branched from feature branch)
|
|
427
|
-
assertTrue(existsSync(join(repo, "s01-work.txt")),
|
|
428
|
-
"multi: S02 has S01 output (inherited via feature branch)");
|
|
429
|
-
|
|
430
|
-
writeFileSync(join(repo, "s02-work.txt"), "s02 output\n");
|
|
431
|
-
run('git add -A && git commit -m "feat(S02): work"', repo);
|
|
432
|
-
|
|
433
|
-
switchToMain(repo);
|
|
434
|
-
assertEq(getCurrentBranch(repo), "feature/big-change",
|
|
435
|
-
"multi: switchToMain goes to feature branch after S02");
|
|
436
|
-
|
|
437
|
-
const s02merge = mergeSliceToMain(repo, "M001", "S02", "Second slice");
|
|
438
|
-
assertEq(getCurrentBranch(repo), "feature/big-change",
|
|
439
|
-
"multi: after S02 merge, on feature branch");
|
|
440
|
-
assertTrue(existsSync(join(repo, "s02-work.txt")),
|
|
441
|
-
"multi: S02 work merged to feature branch");
|
|
442
|
-
assertTrue(existsSync(join(repo, "s01-work.txt")),
|
|
443
|
-
"multi: S01 work still on feature branch after S02 merge");
|
|
444
|
-
assertTrue(s02merge.deletedBranch, "multi: S02 branch deleted");
|
|
445
|
-
|
|
446
|
-
// Final check: main still untouched
|
|
447
|
-
run("git stash", repo);
|
|
448
|
-
run("git checkout main", repo);
|
|
449
|
-
assertTrue(!existsSync(join(repo, "s01-work.txt")),
|
|
450
|
-
"multi: main still lacks S01 work at end");
|
|
451
|
-
assertTrue(!existsSync(join(repo, "s02-work.txt")),
|
|
452
|
-
"multi: main still lacks S02 work at end");
|
|
453
|
-
assertEq(readFileSync(join(repo, "README.md"), "utf-8").trim(), "base",
|
|
454
|
-
"multi: main README unchanged");
|
|
455
|
-
|
|
456
|
-
rmSync(repo, { recursive: true, force: true });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ── Resume scenario: milestone ID re-set after restart ────────────────
|
|
460
|
-
//
|
|
461
|
-
// Simulates crash + restart: the cached GitServiceImpl is lost, but the
|
|
462
|
-
// metadata file persists on disk. Re-calling setActiveMilestoneId should
|
|
463
|
-
// restore integration branch resolution.
|
|
464
|
-
|
|
465
|
-
console.log("\n=== Resume: milestone ID re-set restores integration branch ===");
|
|
466
|
-
|
|
467
|
-
{
|
|
468
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-integ-resume-"));
|
|
469
|
-
run("git init -b main", repo);
|
|
470
|
-
run("git config user.name 'Pi Test'", repo);
|
|
471
|
-
run("git config user.email 'pi@example.com'", repo);
|
|
472
|
-
writeFileSync(join(repo, "README.md"), "init\n");
|
|
473
|
-
run("git add -A && git commit -m init", repo);
|
|
474
|
-
|
|
475
|
-
run("git checkout -b my-feature", repo);
|
|
476
|
-
captureIntegrationBranch(repo, "M001");
|
|
477
|
-
setActiveMilestoneId(repo, "M001");
|
|
478
|
-
|
|
479
|
-
// Create a slice and do some work
|
|
480
|
-
ensureSliceBranch(repo, "M001", "S01");
|
|
481
|
-
writeFileSync(join(repo, "work.txt"), "wip\n");
|
|
482
|
-
run('git add -A && git commit -m "wip"', repo);
|
|
483
|
-
|
|
484
|
-
// Simulate "restart" — clear milestone ID (fresh service instance)
|
|
485
|
-
setActiveMilestoneId(repo, null);
|
|
486
|
-
assertEq(getMainBranch(repo), "main",
|
|
487
|
-
"resume: getMainBranch returns main when milestone cleared");
|
|
488
|
-
|
|
489
|
-
// Re-set milestone ID (what auto.ts does on resume)
|
|
490
|
-
setActiveMilestoneId(repo, "M001");
|
|
491
|
-
assertEq(getMainBranch(repo), "my-feature",
|
|
492
|
-
"resume: getMainBranch returns feature branch after re-set");
|
|
493
|
-
|
|
494
|
-
// Full lifecycle still works after resume
|
|
495
|
-
switchToMain(repo);
|
|
496
|
-
assertEq(getCurrentBranch(repo), "my-feature",
|
|
497
|
-
"resume: switchToMain goes to feature branch after re-set");
|
|
498
|
-
|
|
499
|
-
const result = mergeSliceToMain(repo, "M001", "S01", "Resume slice");
|
|
500
|
-
assertEq(getCurrentBranch(repo), "my-feature",
|
|
501
|
-
"resume: merge lands on feature branch after re-set");
|
|
502
|
-
assertTrue(existsSync(join(repo, "work.txt")),
|
|
503
|
-
"resume: merged work exists on feature branch");
|
|
504
|
-
|
|
505
|
-
rmSync(repo, { recursive: true, force: true });
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ── Backward compat: no metadata file, plain main workflow ────────────
|
|
509
|
-
//
|
|
510
|
-
// Simulates existing projects that were created before this feature.
|
|
511
|
-
// No metadata file exists, milestone ID is set — getMainBranch should
|
|
512
|
-
// still return "main" and the entire slice lifecycle works unchanged.
|
|
513
|
-
|
|
514
|
-
console.log("\n=== Backward compat: no metadata, main workflow ===");
|
|
515
|
-
|
|
516
|
-
{
|
|
517
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-integ-compat-"));
|
|
518
|
-
run("git init -b main", repo);
|
|
519
|
-
run("git config user.name 'Pi Test'", repo);
|
|
520
|
-
run("git config user.email 'pi@example.com'", repo);
|
|
521
|
-
writeFileSync(join(repo, "README.md"), "init\n");
|
|
522
|
-
run("git add -A && git commit -m init", repo);
|
|
523
|
-
|
|
524
|
-
// Set milestone but DON'T capture integration branch (simulates old project)
|
|
525
|
-
setActiveMilestoneId(repo, "M001");
|
|
526
|
-
|
|
527
|
-
assertEq(getMainBranch(repo), "main",
|
|
528
|
-
"compat: getMainBranch returns main without metadata");
|
|
529
|
-
|
|
530
|
-
// Full lifecycle on main still works
|
|
531
|
-
ensureSliceBranch(repo, "M001", "S01");
|
|
532
|
-
writeFileSync(join(repo, "feature.txt"), "new\n");
|
|
533
|
-
run('git add -A && git commit -m "feat: work"', repo);
|
|
534
|
-
|
|
535
|
-
switchToMain(repo);
|
|
536
|
-
assertEq(getCurrentBranch(repo), "main",
|
|
537
|
-
"compat: switchToMain goes to main");
|
|
538
|
-
|
|
539
|
-
const result = mergeSliceToMain(repo, "M001", "S01", "Compat slice");
|
|
540
|
-
assertEq(getCurrentBranch(repo), "main",
|
|
541
|
-
"compat: merge lands on main");
|
|
542
|
-
assertTrue(existsSync(join(repo, "feature.txt")),
|
|
543
|
-
"compat: merged work exists on main");
|
|
544
|
-
assertTrue(result.deletedBranch, "compat: branch deleted");
|
|
545
|
-
|
|
546
|
-
rmSync(repo, { recursive: true, force: true });
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// ── ensureSliceBranch from another slice with integration branch ──────
|
|
550
|
-
//
|
|
551
|
-
// When on gsd/M001/S01 and creating S02, the code falls back to
|
|
552
|
-
// getMainBranch() (not the current slice). With integration branch set,
|
|
553
|
-
// S02 should branch from the feature branch.
|
|
554
|
-
|
|
555
|
-
console.log("\n=== ensureSliceBranch: S02 from S01 uses integration branch as base ===");
|
|
556
|
-
|
|
557
|
-
{
|
|
558
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-integ-chain-"));
|
|
559
|
-
run("git init -b main", repo);
|
|
560
|
-
run("git config user.name 'Pi Test'", repo);
|
|
561
|
-
run("git config user.email 'pi@example.com'", repo);
|
|
562
|
-
writeFileSync(join(repo, "README.md"), "init\n");
|
|
563
|
-
run("git add -A && git commit -m init", repo);
|
|
564
|
-
|
|
565
|
-
run("git checkout -b dev-branch", repo);
|
|
566
|
-
writeFileSync(join(repo, "dev-only.txt"), "from dev\n");
|
|
567
|
-
run('git add -A && git commit -m "dev setup"', repo);
|
|
568
|
-
|
|
569
|
-
captureIntegrationBranch(repo, "M001");
|
|
570
|
-
setActiveMilestoneId(repo, "M001");
|
|
571
|
-
|
|
572
|
-
// Create S01 (from dev-branch)
|
|
573
|
-
ensureSliceBranch(repo, "M001", "S01");
|
|
574
|
-
writeFileSync(join(repo, "s01.txt"), "s01\n");
|
|
575
|
-
run('git add -A && git commit -m "s01 work"', repo);
|
|
576
|
-
|
|
577
|
-
// While on S01, create S02 — should fall back to integration branch
|
|
578
|
-
ensureSliceBranch(repo, "M001", "S02");
|
|
579
|
-
assertEq(getCurrentBranch(repo), "gsd/M001/S02", "chain: on S02");
|
|
580
|
-
|
|
581
|
-
// S02 should be based on dev-branch (the integration branch)
|
|
582
|
-
assertTrue(existsSync(join(repo, "dev-only.txt")),
|
|
583
|
-
"chain: S02 has dev-branch content");
|
|
584
|
-
assertTrue(!existsSync(join(repo, "s01.txt")),
|
|
585
|
-
"chain: S02 does NOT have S01 content (not chained from S01)");
|
|
586
|
-
|
|
587
|
-
rmSync(repo, { recursive: true, force: true });
|
|
588
|
-
}
|
|
589
|
-
|
|
590
165
|
rmSync(base, { recursive: true, force: true });
|
|
591
166
|
report();
|
|
592
167
|
}
|