feat(FN-3428): add project-node path mapping APIs and agent detail review-b

Merges FN-3428's branch-field contract and merge target override/default behavior tests across core, dashboard, and engine, plus a companion fix that sweeps subsumed autostash orphans and surfaces outcomes on the task feed. Adds project-node path mapping persistence APIs and schema (FN-3503), new ag

Fusion-Task-Id: FN-3428
This commit is contained in:
Fusion
2026-05-07 12:43:55 -07:00
committed by gsxdsm
parent a1ae903d2d
commit 455819ac75
6 changed files with 180 additions and 10 deletions

View File

@@ -457,6 +457,17 @@ describe("updateTask", () => {
});
});
it("omits branch and baseBranch from update payload when unset", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_TASK, title: "Updated" }));
await updateTask("FN-001", { title: "Updated" });
const call = vi.mocked(globalThis.fetch).mock.calls[0];
const body = JSON.parse((call[1] as RequestInit).body as string);
expect(body).not.toHaveProperty("branch");
expect(body).not.toHaveProperty("baseBranch");
});
it("sends sourceIssue object when source metadata is provided", async () => {
const sourceIssue = {
provider: "github",
@@ -588,6 +599,17 @@ describe("createTask", () => {
expect(body.baseBranch).toBe("main");
});
it("omits branch and baseBranch in create payload when unset", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_CREATED_TASK));
await createTask({ description: "Task without branch fields" });
const call = vi.mocked(globalThis.fetch).mock.calls[0];
const body = JSON.parse((call[1] as RequestInit).body as string);
expect(body).not.toHaveProperty("branch");
expect(body).not.toHaveProperty("baseBranch");
});
it("serializes nodeId in create payload when execution target is specified", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, {
...FAKE_CREATED_TASK,

View File

@@ -664,6 +664,27 @@ describe("Board", () => {
expect(todoTasks.map((t: Task) => t.id).sort()).toEqual(["FN-999", "SEARCH-123"]);
});
it("renders server-filtered branch-target results without additional client filtering", () => {
const branchFilteredTasks: Task[] = [
createTask({
id: "FN-3428",
description: "Task targeting release branch",
column: "todo",
branch: "feature/fn-3428",
baseBranch: "release/2026-05",
}),
];
renderBoard({ tasks: branchFilteredTasks, searchQuery: "release/2026-05" });
const todoColumn = screen.getByTestId("column-todo");
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]") as Task[];
expect(todoTasks).toHaveLength(1);
expect(todoTasks[0]?.id).toBe("FN-3428");
expect(todoTasks[0]?.branch).toBe("feature/fn-3428");
expect(todoTasks[0]?.baseBranch).toBe("release/2026-05");
});
it("shows all tasks for whitespace-only search query (server treats as empty)", () => {
const tasks: Task[] = [
createTask({ id: "FN-001", description: "First task", column: "todo" }),

View File

@@ -235,6 +235,32 @@ describe("TaskCard", () => {
expect(screen.queryByText("Branch")).toBeNull();
});
it("renders merge target from task.baseBranch, not prInfo.baseBranch metadata", () => {
render(
<TaskCard
task={makeTask({
branch: "fusion/fn-001",
baseBranch: "release/task-target",
prInfo: {
url: "https://github.com/runfusion/fusion/pull/10",
number: 10,
status: "open",
title: "PR title",
headBranch: "feature/pr-head",
baseBranch: "main",
commentCount: 0,
},
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(screen.getByText("Base")).toBeDefined();
expect(screen.getByText("release/task-target")).toBeDefined();
expect(screen.queryByText("main")).toBeNull();
});
it("shows both chips when branch and base branch are both non-default", () => {
const { container } = render(
<TaskCard

View File

@@ -1113,6 +1113,32 @@ describe("TaskDetailModal", () => {
});
});
it("saves changed baseBranch independently of branch", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
const { container } = render(
<TaskDetailModal
task={makeTask({ id: "FN-001", column: "todo", branch: "feature/fn-3422", baseBranch: "develop" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.change(container.querySelector("#task-base-branch") as HTMLInputElement, { target: { value: "release/2026-05" } });
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { baseBranch: "release/2026-05" }, undefined);
});
});
it("sends null branch fields when working/base branches are cleared", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);