fix(gsd): use milestone branch for merged worktree cleanup

This commit is contained in:
mastertyko 2026-04-12 18:45:36 +02:00
parent 791ce1b35e
commit 8a37e2ce10
2 changed files with 67 additions and 2 deletions

View file

@ -2043,7 +2043,7 @@ export function mergeMilestoneToMain(
// 12. Remove worktree directory first (must happen before branch deletion)
try {
removeWorktree(originalBasePath_, milestoneId, {
branch: null as unknown as string,
branch: milestoneBranch,
deleteBranch: false,
});
} catch (err) {

View file

@ -12,7 +12,7 @@
import { describe, test, afterEach } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, readFileSync } from "node:fs";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, readFileSync, symlinkSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
@ -44,6 +44,27 @@ function createTempRepo(): string {
return dir;
}
function createTempRepoWithExternalGsd(): { repo: string; externalState: string } {
const realTmp = realpathSync(tmpdir());
const repo = realpathSync(mkdtempSync(join(realTmp, "wt-ms-merge-ext-test-")));
const externalState = realpathSync(mkdtempSync(join(realTmp, "wt-ms-merge-ext-state-")));
run("git init", repo);
run("git config user.email test@test.com", repo);
run("git config user.name Test", repo);
mkdirSync(join(externalState, "worktrees"), { recursive: true });
symlinkSync(externalState, join(repo, ".gsd"));
writeFileSync(join(repo, "README.md"), "# test\n");
writeFileSync(join(externalState, "STATE.md"), "# State\n");
run("git add .", repo);
run("git commit -m init", repo);
run("git branch -M main", repo);
return { repo, externalState };
}
/** Minimal roadmap content for mergeMilestoneToMain. */
function makeRoadmap(milestoneId: string, title: string, slices: Array<{ id: string; title: string }>): string {
const sliceLines = slices.map(s => `- [x] **${s.id}: ${s.title}**`).join("\n");
@ -87,6 +108,12 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
return d;
}
function freshRepoWithExternalGsd(): { repo: string; externalState: string } {
const { repo, externalState } = createTempRepoWithExternalGsd();
tempDirs.push(repo, externalState);
return { repo, externalState };
}
afterEach(() => {
process.chdir(savedCwd);
for (const d of tempDirs) {
@ -638,6 +665,44 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
"#1906: codeFilesChanged must be false when only .gsd/ files were merged");
});
test("#2156: mergeMilestoneToMain removes external-state worktrees using the milestone branch name", () => {
const { repo, externalState } = freshRepoWithExternalGsd();
const wtPath = createAutoWorktree(repo, "M215");
addSliceToMilestone(repo, wtPath, "M215", "S01", "External cleanup", [
{ file: "external-cleanup.ts", content: "export const externalCleanup = true;\n", message: "add external cleanup" },
]);
const realWtPath = realpathSync(wtPath);
assert.ok(
realWtPath.startsWith(externalState),
`worktree should be registered under external .gsd state, got ${realWtPath}`,
);
// Recreate the exact divergence from #1852: local .gsd/ is replaced with a
// stale real directory, so worktreePath() no longer matches git's record.
unlinkSync(join(repo, ".gsd"));
mkdirSync(join(repo, ".gsd", "worktrees", "M215"), { recursive: true });
writeFileSync(join(repo, ".gsd", "STATE.md"), "# Local stale state\n");
writeFileSync(join(repo, ".gsd", "worktrees", "M215", "stale.txt"), "stale local artifact\n");
const roadmap = makeRoadmap("M215", "External cleanup", [
{ id: "S01", title: "External cleanup" },
]);
mergeMilestoneToMain(repo, "M215", roadmap);
assert.ok(
!run("git worktree list", repo).includes("M215"),
"merged milestone worktree should be removed from git worktree list",
);
assert.ok(!existsSync(realWtPath), "real external worktree directory should be removed");
assert.ok(
!run("git branch", repo).includes("milestone/M215"),
"milestone branch should be deleted after merge cleanup",
);
});
test("#2912: MERGE_HEAD cleaned up after squash-merge conflict", () => {
const repo = freshRepo();
const wtPath = createAutoWorktree(repo, "M291");