When mergeAndExit cannot find the roadmap at the project root, it now tries the worktree path as a fallback. If neither location has a roadmap, the teardown preserves the branch (preserveBranch: true) so commits are not orphaned when the worktree is pruned. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0483363a33
commit
4367ea36c4
3 changed files with 85 additions and 8 deletions
|
|
@ -142,3 +142,25 @@ test("auto/phases.ts milestone transition block contains worktree lifecycle", ()
|
|||
"auto/phases.ts should call resolver.enterMilestone for incoming milestone",
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Verify worktree-resolver mergeAndExit preserves branch on missing roadmap (#1573) ──
|
||||
|
||||
test("worktree-resolver mergeAndExit preserves branch when roadmap is missing (#1573)", () => {
|
||||
const resolverSrc = readFileSync(
|
||||
join(__dirname, "..", "worktree-resolver.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
// The fallback teardown must pass preserveBranch: true to prevent orphaning commits
|
||||
assert.ok(
|
||||
resolverSrc.includes("preserveBranch: true"),
|
||||
"worktree-resolver.ts should pass preserveBranch: true in the no-roadmap fallback",
|
||||
);
|
||||
|
||||
// The worktree path should be tried as a fallback for roadmap resolution
|
||||
assert.ok(
|
||||
resolverSrc.includes("this.s.basePath !== originalBase") ||
|
||||
resolverSrc.includes("roadmap-fallback"),
|
||||
"worktree-resolver.ts should try resolving roadmap from worktree path as fallback",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@ test("mergeAndExit in worktree mode shows pushed status", () => {
|
|||
assert.ok(ctx.messages.some((m) => m.msg.includes("Pushed to remote")));
|
||||
});
|
||||
|
||||
test("mergeAndExit falls back to teardown when roadmap is missing", () => {
|
||||
test("mergeAndExit falls back to teardown with preserveBranch when roadmap is missing (#1573)", () => {
|
||||
const s = makeSession({
|
||||
basePath: "/project/.gsd/worktrees/M001",
|
||||
originalBasePath: "/project",
|
||||
|
|
@ -449,10 +449,42 @@ test("mergeAndExit falls back to teardown when roadmap is missing", () => {
|
|||
|
||||
resolver.mergeAndExit("M001", ctx);
|
||||
|
||||
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 1);
|
||||
const teardownCalls = findCalls(deps.calls, "teardownAutoWorktree");
|
||||
assert.equal(teardownCalls.length, 1);
|
||||
// Branch must be preserved so commits are not orphaned (#1573)
|
||||
assert.deepEqual(teardownCalls[0].args[2], { preserveBranch: true });
|
||||
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
||||
assert.equal(s.basePath, "/project"); // restored
|
||||
assert.ok(ctx.messages.some((m) => m.msg.includes("no roadmap for merge")));
|
||||
assert.ok(ctx.messages.some((m) => m.msg.includes("branch preserved")));
|
||||
});
|
||||
|
||||
test("mergeAndExit resolves roadmap from worktree when missing at project root (#1573)", () => {
|
||||
const s = makeSession({
|
||||
basePath: "/project/.gsd/worktrees/M001",
|
||||
originalBasePath: "/project",
|
||||
});
|
||||
// resolveMilestoneFile returns null for project root, returns path for worktree
|
||||
const deps = makeDeps({
|
||||
isInAutoWorktree: () => true,
|
||||
getIsolationMode: () => "worktree",
|
||||
resolveMilestoneFile: (basePath: string) => {
|
||||
if (basePath === "/project") return null; // missing at project root
|
||||
if (basePath === "/project/.gsd/worktrees/M001") {
|
||||
return "/project/.gsd/worktrees/M001/.gsd/milestones/M001/M001-ROADMAP.md";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
const ctx = makeNotifyCtx();
|
||||
const resolver = new WorktreeResolver(s, deps);
|
||||
|
||||
resolver.mergeAndExit("M001", ctx);
|
||||
|
||||
// Should have called mergeMilestoneToMain, not bare teardown
|
||||
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
|
||||
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
|
||||
assert.equal(s.basePath, "/project"); // restored
|
||||
assert.ok(ctx.messages.some((m) => m.msg.includes("merged to main")));
|
||||
});
|
||||
|
||||
test("mergeAndExit in worktree mode restores to project root on merge failure", () => {
|
||||
|
|
|
|||
|
|
@ -338,11 +338,31 @@ export class WorktreeResolver {
|
|||
});
|
||||
}
|
||||
|
||||
const roadmapPath = this.deps.resolveMilestoneFile(
|
||||
// Resolve roadmap — try project root first, then worktree path as fallback.
|
||||
// The worktree may hold the only copy when syncWorktreeStateBack fails
|
||||
// silently or .gsd/ is not symlinked. Without the fallback, a missing
|
||||
// roadmap triggers bare teardown which deletes the branch and orphans all
|
||||
// milestone commits (#1573).
|
||||
let roadmapPath = this.deps.resolveMilestoneFile(
|
||||
originalBase,
|
||||
milestoneId,
|
||||
"ROADMAP",
|
||||
);
|
||||
if (!roadmapPath && this.s.basePath !== originalBase) {
|
||||
roadmapPath = this.deps.resolveMilestoneFile(
|
||||
this.s.basePath,
|
||||
milestoneId,
|
||||
"ROADMAP",
|
||||
);
|
||||
if (roadmapPath) {
|
||||
debugLog("WorktreeResolver", {
|
||||
action: "mergeAndExit",
|
||||
milestoneId,
|
||||
phase: "roadmap-fallback",
|
||||
note: "resolved from worktree path",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (roadmapPath) {
|
||||
const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
|
||||
|
|
@ -356,11 +376,14 @@ export class WorktreeResolver {
|
|||
"info",
|
||||
);
|
||||
} else {
|
||||
// No roadmap — fall back to bare teardown
|
||||
this.deps.teardownAutoWorktree(originalBase, milestoneId);
|
||||
// No roadmap at either location — teardown but PRESERVE the branch so
|
||||
// commits are not orphaned. The user can merge manually later (#1573).
|
||||
this.deps.teardownAutoWorktree(originalBase, milestoneId, {
|
||||
preserveBranch: true,
|
||||
});
|
||||
ctx.notify(
|
||||
`Exited worktree for ${milestoneId} (no roadmap for merge).`,
|
||||
"info",
|
||||
`Exited worktree for ${milestoneId} (no roadmap found — branch preserved for manual merge).`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue