FN-5818: fix shared-branch-group execution to use per-task working branches
Ensure shared branch-group flows consistently derive and acquire per-task working branches. - update engine merge/executor/scheduler/self-healing paths to resolve task-scoped working branch names instead of shared branch-group names - adjust worktree acquisition and already-merged detection logic to use the corrected branch resolution - add regression coverage for shared-branch-group working-branch behavior, worktree acquisition, and worktree name derivation - add a patch changeset for @runfusion/fusion Files changed: .changeset/fn-5818-shared-branch-working-branch.md | 5 ++ .../shared-branch-group-working-branch.test.ts | 53 ++++++++++++++++++++++ .../src/__tests__/worktree-acquisition.test.ts | 52 +++++++++++++++++++++ .../engine/src/__tests__/worktree-names.test.ts | 20 +++++++- packages/engine/src/already-merged-detector.ts | 6 +-- packages/engine/src/executor.ts | 10 ++-- packages/engine/src/merger-ai.ts | 4 +- packages/engine/src/merger.ts | 16 +++---- packages/engine/src/scheduler.ts | 6 +-- packages/engine/src/self-healing.ts | 12 ++--- packages/engine/src/worktree-acquisition.ts | 4 +- packages/engine/src/worktree-names.ts | 9 +++- 12 files changed, 166 insertions(+), 31 deletions(-) Fusion-Task-Id: FN-5818 Fusion-Task-Lineage: c7eb1ec4-971a-49ad-9156-5c0349c629aa
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { acquireTaskWorktree } from "../../worktree-acquisition.js";
|
||||
|
||||
describe("shared branch group working branch regression", () => {
|
||||
it("uses per-task working branches for shared members and keeps existing derivation modes", async () => {
|
||||
const store = {
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
} as any;
|
||||
|
||||
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||
const shared = { assignmentMode: "shared", groupId: "BG-1", source: "planning" } as const;
|
||||
|
||||
const [a, b] = await Promise.all([
|
||||
acquireTaskWorktree({
|
||||
task: { id: "FN-201", title: "a", description: "a", branch: "clionboarding", branchContext: shared, worktree: null } as any,
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
}),
|
||||
acquireTaskWorktree({
|
||||
task: { id: "FN-202", title: "b", description: "b", branch: "clionboarding", branchContext: shared, worktree: null } as any,
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(a.branch).toBe("fusion/fn-201");
|
||||
expect(b.branch).toBe("fusion/fn-202");
|
||||
expect(a.branch).not.toBe(b.branch);
|
||||
|
||||
const perTask = await acquireTaskWorktree({
|
||||
task: { id: "FN-203", title: "c", description: "c", branch: "fusion/custom", branchContext: { assignmentMode: "per-task-derived", groupId: "BG-1", source: "planning" }, worktree: null } as any,
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
});
|
||||
const ungrouped = await acquireTaskWorktree({
|
||||
task: { id: "FN-204", title: "d", description: "d", branch: null, worktree: null } as any,
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
});
|
||||
|
||||
expect(perTask.branch).toBe("fusion/custom");
|
||||
expect(ungrouped.branch).toBe("fusion/fn-204");
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,58 @@ describe("acquireTaskWorktree", () => {
|
||||
expect(vi.mocked(branchConflicts.reanchorBranchToBase)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("derives distinct per-task working branches for shared branch-group members", async () => {
|
||||
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||
const sharedBranch = "clionboarding";
|
||||
const sharedContext = { assignmentMode: "shared", groupId: "BG-1", source: "planning" } as const;
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
acquireTaskWorktree({
|
||||
task: { ...task, id: "FN-100", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
}),
|
||||
acquireTaskWorktree({
|
||||
task: { ...task, id: "FN-101", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(first.branch).toBe("fusion/fn-100");
|
||||
expect(second.branch).toBe("fusion/fn-101");
|
||||
expect(first.branch).not.toBe(second.branch);
|
||||
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-100", expect.any(String), "FN-100", undefined, false);
|
||||
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-101", expect.any(String), "FN-101", undefined, false);
|
||||
});
|
||||
|
||||
it("keeps per-task-derived and ungrouped branch derivation unchanged", async () => {
|
||||
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||
|
||||
const perTaskDerived = await acquireTaskWorktree({
|
||||
task: { ...task, id: "FN-102", worktree: null, branch: "fusion/custom-derived", branchContext: { assignmentMode: "per-task-derived", groupId: "BG-1", source: "planning" } },
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
});
|
||||
|
||||
const ungrouped = await acquireTaskWorktree({
|
||||
task: { ...task, id: "FN-103", worktree: null, branch: null },
|
||||
rootDir: process.cwd(),
|
||||
store,
|
||||
settings: {},
|
||||
createWorktree,
|
||||
});
|
||||
|
||||
expect(perTaskDerived.branch).toBe("fusion/custom-derived");
|
||||
expect(ungrouped.branch).toBe("fusion/fn-103");
|
||||
});
|
||||
|
||||
it("acquires from pool when enabled", async () => {
|
||||
const prepareForTask = vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pooled", reclaimed: false });
|
||||
const release = vi.fn();
|
||||
|
||||
@@ -2,7 +2,25 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { generateWorktreeName, ADJECTIVES, NOUNS } from "../worktree-names.js";
|
||||
import { generateWorktreeName, ADJECTIVES, NOUNS, resolveTaskWorkingBranch } from "../worktree-names.js";
|
||||
|
||||
describe("resolveTaskWorkingBranch", () => {
|
||||
it("returns canonical per-task branch for shared assignment mode", () => {
|
||||
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "clionboarding", branchContext: { assignmentMode: "shared", groupId: "bg-1", source: "planning" } })).toBe("fusion/fn-5818");
|
||||
});
|
||||
|
||||
it("returns explicit branch for per-task-derived assignment mode", () => {
|
||||
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "fusion/custom", branchContext: { assignmentMode: "per-task-derived", groupId: "bg-1", source: "planning" } })).toBe("fusion/custom");
|
||||
});
|
||||
|
||||
it("returns canonical branch for ungrouped task without branch", () => {
|
||||
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: undefined })).toBe("fusion/fn-5818");
|
||||
});
|
||||
|
||||
it("returns explicit branch for ungrouped task with branch", () => {
|
||||
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "feature/fn-5818" })).toBe("feature/fn-5818");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateWorktreeName", () => {
|
||||
let tempDir: string;
|
||||
|
||||
Reference in New Issue
Block a user