FN-5671: add branch-strategy selection to new task creation

Add branch-strategy based task branch handling from modal through task creation routes.

- add branch strategy dropdown + validation in New Task modal and TaskForm, including required branch-name handling for existing/custom-new modes
- send structured branchSelection payload from dashboard API client and cover UI/API behavior with new tests
- add server-side branch selection mode parsing, auto-new branch derivation (fusion/<task-id>-<slug>), and response updates when auto branch is generated
- update engine/worktree and dashboard route tests plus dashboard guide documentation
- add a changeset for @runfusion/fusion patch release

Files changed:
 .changeset/fn-5671-branch-strategy-dropdown.md     |  7 ++
 docs/dashboard-guide.md                            | 16 +++++
 packages/dashboard/app/__tests__/api-tasks.test.ts | 21 ++++++
 packages/dashboard/app/api/legacy.ts               |  9 +++
 packages/dashboard/app/components/NewTaskModal.css |  8 +++
 packages/dashboard/app/components/NewTaskModal.tsx | 41 +++++++++---
 packages/dashboard/app/components/TaskForm.tsx     | 30 ++++++++-
 .../app/components/__tests__/NewTaskModal.test.tsx | 77 +++++++++++++++++++---
 .../src/__tests__/branch-selection.test.ts         | 13 ++++
 .../dashboard/src/__tests__/routes-tasks.test.ts   | 72 ++++++++++++++++++++
 packages/dashboard/src/routes/branch-selection.ts  | 45 ++++++++-----
 .../src/routes/register-task-workflow-routes.ts    | 22 +++++--
 .../engine/src/__tests__/worktree-pool.test.ts     | 41 ++++++++++++
 packages/engine/src/worktree-pool.ts               |  2 +-
 14 files changed, 361 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-5671
Fusion-Task-Lineage: 7f2e7b68-050d-4a2c-af0b-cb895de35576
This commit is contained in:
gsxdsm
2026-05-29 09:44:06 -07:00
parent 16d20063bf
commit 4fee2c1324
14 changed files with 376 additions and 58 deletions

View File

@@ -363,6 +363,46 @@ describe("WorktreePool", () => {
});
});
it("maps slugged fusion branches to canonical task IDs", async () => {
mockedExistsSync.mockReturnValue(true);
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr === 'git checkout -B "fusion/fn-5671-add-dropdown" main') {
const err: any = new Error("branch conflict");
err.stderr = Buffer.from("fatal: 'fusion/fn-5671-add-dropdown' is already used by worktree at '/other/wt'");
throw err;
}
if (cmdStr === "git worktree list --porcelain") {
return Buffer.from(["worktree /other/wt", "HEAD 1111111", "branch refs/heads/fusion/fn-5671-add-dropdown", ""].join("\n"));
}
if (cmdStr.includes("git rev-parse --verify 'fusion/fn-5671-add-dropdown^{commit}'")) {
return Buffer.from("abc123def456\n");
}
return Buffer.from("");
});
const inspectSpy = vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
kind: "live-foreign",
livePath: "/other/wt",
error: new BranchConflictError({
branchName: "fusion/fn-5671-add-dropdown",
conflictingWorktreePath: "/other/wt",
existingTipSha: "abc123def456",
strandedCommits: [],
startPoint: "main",
recommendedAction: "Inspect/reclaim.",
}),
});
await expect(pool.prepareForTask("/tmp/wt", "fusion/fn-5671-add-dropdown")).rejects.toBeInstanceOf(BranchConflictError);
expect(inspectSpy).toHaveBeenCalledWith(expect.objectContaining({
branchName: "fusion/fn-5671-add-dropdown",
ownerTaskId: "FN-5671",
requestingTaskId: "FN-5671",
}));
});
it("throws BranchConflictError for cross-task live-foreign conflicts", async () => {
mockedExistsSync.mockReturnValue(true);
@@ -897,6 +937,7 @@ describe("cleanupOrphanedWorktrees", () => {
expect(removeCalls[0][0]).not.toContain("active-wt");
});
it("handles git worktree remove failures gracefully (non-fatal)", async () => {
mockedReaddirSync.mockReturnValue([
makeDirEntry("fail-wt"),

View File

@@ -252,7 +252,7 @@ export function isInsideWorktreesDir(
* up via {@link cleanupOrphanedWorktrees}.
*/
function deriveTaskIdFromBranch(branchName: string): string {
const match = branchName.match(/^fusion\/(fn-\d+)(?:-\d+)?$/i);
const match = branchName.match(/^fusion\/(fn-\d+)(?:-\d+)?(?:-[a-z0-9._-]+)*$/i);
return match ? match[1].toUpperCase() : branchName.toUpperCase();
}