feat(FN-709): store branch name on tasks for reliable merge recovery

- Add 'branch' field to Task and ArchivedTaskEntry types with DB migration
- Executor stores branch name on task after worktree assignment
- Merger reads branch from task metadata instead of relying on worktree state
- Implement non-destructive conflict recovery in prepareForTask with reset/clean
- Add tests for branch storage, merger branch reading, and worktree recovery
This commit is contained in:
gsxdsm
2026-04-02 14:08:31 -07:00
parent 932a45a862
commit 321feb0b8d
10 changed files with 241 additions and 78 deletions

View File

@@ -211,6 +211,54 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
});
});
describe("aiMergeTask — task.branch field", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedExistsSync.mockReturnValue(true);
setupHappyPathExecSync();
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
},
} as any);
});
it("uses task.branch when set instead of deriving from task ID", async () => {
const store = createMockStore(
{ id: "FN-050", branch: "kb/fn-050-2", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
// Should use kb/fn-050-2, not kb/fn-050
expect(result.branch).toBe("kb/fn-050-2");
// Verify the suffixed branch was verified and deleted
const revParseCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("rev-parse --verify") && String(call[0]).includes("kb/fn-050-2"),
);
expect(revParseCall).toBeDefined();
const branchDeleteCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("branch -d") && String(call[0]).includes("kb/fn-050-2"),
);
expect(branchDeleteCall).toBeDefined();
});
it("falls back to conventional branch name when task.branch is not set", async () => {
const store = createMockStore(
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
expect(result.branch).toBe("kb/fn-050");
});
});
describe("aiMergeTask — empty squash merge (branch already merged via dep)", () => {
beforeEach(() => {
vi.clearAllMocks();