fix: preserve reclaimed worktree branch provenance (#3507)
## Summary - persist engine branch-write provenance when reclaiming an existing task worktree - cover branch-conflict reclaim with a regression assertion for the branch, worktree, and provenance tuple ## Test plan - `pnpm --filter @fusion/engine exec vitest run src/__tests__/executor-worktree.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/engine typecheck` - `pnpm check:changesets -- --strict` - `pnpm check:fnxc-future-dates` - `pnpm build` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved recovery when reclaiming existing task worktrees. * Preserved task branch details and worktree paths during branch-conflict recovery. * Recorded whether branch updates originated from the system or an operator for more reliable task state tracking. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
7
.changeset/fix-worktree-reclaim-branch-provenance.md
Normal file
7
.changeset/fix-worktree-reclaim-branch-provenance.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Preserve task branches when Fusion reclaims an existing task worktree.
|
||||
category: fix
|
||||
dev: Supplies engine branch-write provenance during branch-conflict reclamation.
|
||||
@@ -1175,7 +1175,58 @@ describe("TaskExecutor worktree recovery", () => {
|
||||
|
||||
expect(result).toBe("reclaimed");
|
||||
expect(normalize).toHaveBeenCalledWith(conflictPath, targetPath, "FN-8400", expect.objectContaining({ worktreesDir: ".worktrees" }));
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-8400", expect.objectContaining({ worktree: targetPath }));
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
"FN-8400",
|
||||
expect.objectContaining({
|
||||
worktree: targetPath,
|
||||
branch: "fusion/fn-8400",
|
||||
branchWriteOrigin: "engine",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves operator provenance when reclaiming an operator-owned Fusion-namespaced branch", async () => {
|
||||
const store = createMockStore();
|
||||
store.getSettings.mockResolvedValue({ worktreesDir: ".worktrees" } as any);
|
||||
const executor = createWorktreeExecutor(store, "/tmp/test");
|
||||
const conflictPath = "/tmp/legacy-worktrees/recover-fn-8401";
|
||||
const targetPath = "/tmp/test/.worktrees/recover-fn-8401";
|
||||
const branch = "fusion/fn-8401";
|
||||
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
|
||||
kind: "reclaimable",
|
||||
livePath: conflictPath,
|
||||
tipSha: "70b47804bc6f27659638e17ac7cf279ed343ff6f",
|
||||
taskAttributedCommitCount: 1,
|
||||
strandedCommits: [{ sha: "70b47804bc6f27659638e17ac7cf279ed343ff6f", subject: "fix(FN-8401): preserve implementation" }],
|
||||
} as any);
|
||||
vi.spyOn(executor as any, "normalizeReclaimableWorktreePath").mockResolvedValue(targetPath);
|
||||
|
||||
const result = await (executor as any).handleBranchConflict(
|
||||
{
|
||||
...makeTask("FN-8401"),
|
||||
branch,
|
||||
worktree: conflictPath,
|
||||
branchContext: { branchOverride: { by: "operator", at: "2026-08-22T22:00:00.000Z", branch } },
|
||||
},
|
||||
new BranchConflictError({
|
||||
branchName: branch,
|
||||
conflictingWorktreePath: conflictPath,
|
||||
existingTipSha: "70b47804bc6f27659638e17ac7cf279ed343ff6f",
|
||||
strandedCommits: [],
|
||||
startPoint: "main",
|
||||
recommendedAction: "reclaim",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBe("reclaimed");
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
"FN-8401",
|
||||
expect.objectContaining({
|
||||
worktree: targetPath,
|
||||
branch,
|
||||
branchWriteOrigin: "operator",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the task-pinned target when normalizing a branch-conflict reclaim", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { isFusionDeletableBranch, type Settings, type Task, type TaskStore } from "@fusion/core";
|
||||
import { classifyTaskBranchOrigin, isFusionDeletableBranch, type Settings, type Task, type TaskStore } from "@fusion/core";
|
||||
import {
|
||||
assertCleanBranchAtBase,
|
||||
BranchConflictError,
|
||||
@@ -55,7 +55,11 @@ export async function reclaimExistingWorktree(
|
||||
): Promise<void> {
|
||||
const targetPath = preservedWorktreeTargetPathForTask(task.id, livePath, settings, deps.rootDir);
|
||||
const normalizedPath = await deps.normalizeReclaimableWorktreePath(livePath, targetPath, task.id, settings);
|
||||
await deps.store.updateTask(task.id, { worktree: normalizedPath, branch });
|
||||
await deps.store.updateTask(task.id, {
|
||||
worktree: normalizedPath,
|
||||
branch,
|
||||
branchWriteOrigin: classifyTaskBranchOrigin(task, branch) === "operator-supplied" ? "operator" : "engine",
|
||||
});
|
||||
const latestTask = await deps.store.getTask(task.id);
|
||||
const baseRef = await resolveDiffBaseRef(normalizedPath, latestTask.baseCommitSha);
|
||||
if (baseRef) {
|
||||
|
||||
Reference in New Issue
Block a user