Files
fusion/packages/dashboard/app/components/__tests__/TodoView.test.tsx
Fusion b0cda2398b feat(FN-2923): merge fusion/fn-2923
- Add task `source` provenance field tracking how tasks enter the system (e.g. `"manual"`, `"heartbeat"`, `"executor"`, `"triage"`, `"cron"`, `"routine"`)
- Wire `source` into shared create-task tools, executor agent, triage agent, cron runner, and routine runner
- Add `source` to heartbeat task creation in `agent-heartbeat.ts` and mission execution loop in `mission-execution-loop.ts`
- Wire `source` field into dashboard routes (`register-planning-subtask-routes.ts`, `register-task-workflow-routes.ts`) and GitHub integration (`register-git-github.ts`)
- Add `source` to the API layer and task store handlers in the dashboard
- Expose `source` in the `fn` CLI task command and `task-plan` extension tool
- Add integration tests for provenance across engine (agent-tools, agent-heartbeat, cron-runner, triage, pr-comment-handler) and dashboard routes
- Update existing tests to assert the correct source values
- Bulk-revert stale changeset files that were superseded by the v0.9.1 release commit

Commits merged:
- test(FN-2923): complete Step 14 — align tests with task source provenance
- test(FN-2923): complete Steps 12-13 — update and add provenance tests
- feat(FN-2923): complete Step 10 — wire CLI task provenance
- feat(FN-2923): complete Step 9 — wire dashboard UI and API source fields
- feat(FN-2923): complete Step 8 — wire dashboard route provenance
- feat(FN-2923): complete Step 7 — wire remaining engine provenance
- feat(FN-2923): complete Step 6 — add routine create-task provenance
- feat(FN-2923): complete Step 5 — add cron create-task provenance
- feat(FN-2923): complete Step 4 — add triage subtask source
- feat(FN-2923): complete Step 3 — add executor create-task source
- feat(FN-2923): complete Step 2 — add heartbeat task provenance
- feat(FN-2923): complete Step 1 — add provenance to shared create-task tools
- feat(FN-2945): merge fusion/fn-2945
- chore(release): v0.9.1

Files changed:
.changeset/active-agents-no-stuck-connecting.md    | 13 ---
 .changeset/active-agents-panel-hoist-heartbeat.md  | 13 ---
 .changeset/fix-agent-heartbeat-terminal-links.md   |  5 --
 .changeset/fix-experimental-defaults.md            |  5 --
 .changeset/improve-git-manager-diff-pane.md        |  5 --
 .changeset/insights-two-pane-layout.md             |  5 --
 .changeset/show-planning-tasks-immediately.md      |  5 --
 CHANGELOG.md                                       | 92 ++++++++++++++++++++++
 package.json                                       |  2 +-
 packages/cli-alias/CHANGELOG.md                    | 15 ++++
 packages/cli-alias/package.json                    |  2 +-
 packages/cli/CHANGELOG.md                          | 12 +++
 packages/cli/package.json                          |  2 +-
 packages/cli/src/__tests__/task-plan.test.ts       |  1 +
 packages/cli/src/commands/__tests__/task.test.ts   | 15 +++-
 packages/cli/src/commands/task.ts                  | 15 +++-
 packages/cli/src/extension.ts                      |  9 +++
 packages/core/CHANGELOG.md                         |  7 ++
 packages/core/package.json                         |  2 +-
 packages/dashboard/CHANGELOG.md                    | 14 ++++
 .../app/__tests__/agent-css-classes.test.ts        |  3 +-
 packages/dashboard/app/__tests__/api.test.ts       | 13 +++
 packages/dashboard/app/api/legacy.ts               |  2 +
 packages/dashboard/app/components/AgentsView.css   | 22 +++---
 .../app/components/ModelOnboardingModal.tsx        |  5 +-
 packages/dashboard/app/components/TodoView.tsx     |  2 +
 .../__tests__/ModelOnboardingModal.test.tsx        |  4 +-
 .../app/components/__tests__/QuickChatFAB.test.tsx | 14 +---
 .../app/components/__tests__/TodoView.test.tsx     |  4 +-
 .../__tests__/agents-view-mobile.test.tsx          |  5 +-
 .../components/__tests__/onboarding-flow.test.tsx  |  2 +-
 .../app/hooks/__tests__/useTaskHandlers.test.ts    |  4 +-
 packages/dashboard/app/hooks/useTaskHandlers.ts    |  4 +-
 packages/dashboard/package.json                    |  2 +-
 packages/dashboard/src/__tests__/routes.test.ts    | 14 ++++
 packages/dashboard/src/routes.ts                   |  4 +
 .../dashboard/src/routes/register-git-github.ts    | 12 +++
 .../src/routes/register-planning-subtask-routes.ts |  3 +
 .../src/routes/register-task-workflow-routes.ts    |  7 ++
 packages/desktop/CHANGELOG.md                      |  7 ++
 packages/desktop/package.json                      |  2 +-
 packages/engine/CHANGELOG.md                       | 11 +++
 packages/engine/package.json                       |  2 +-
 .../engine/src/__tests__/agent-heartbeat.test.ts   | 10 +++
 .../src/__tests__/agent-tools-delegation.test.ts   |  2 +
 packages/engine/src/__tests__/agent-tools.test.ts  | 37 +++++++++
 packages/engine/src/__tests__/cron-runner.test.ts  |  4 +
 .../src/__tests__/pr-comment-handler.test.ts       |  8 ++
 packages/engine/src/__tests__/triage.test.ts       |  2 +
 packages/engine/src/agent-heartbeat.ts             | 13 ++-
 packages/engine/src/agent-tools.ts                 | 13 ++-
 packages/engine/src/cron-runner.ts                 |  7 +-
 packages/engine/src/executor.ts                    |  2 +-
 packages/engine/src/mission-execution-loop.ts      |  8 ++
 packages/engine/src/pr-comment-handler.ts          |  5 ++
 packages/engine/src/project-engine.ts              |  8 ++
 packages/engine/src/routine-runner.ts              |  4 +
 packages/engine/src/triage.ts                      |  4 +
 packages/mobile/CHANGELOG.md                       |  7 ++
 packages/mobile/package.json                       |  2 +-
 packages/pi-claude-cli/CHANGELOG.md                |  7 ++
 packages/pi-claude-cli/package.json                |  2 +-
 packages/plugin-sdk/CHANGELOG.md                   | 10 +++
 packages/plugin-sdk/package.json                   |  2 +-
 .../examples/fusion-plugin-auto-label/CHANGELOG.md |  8 ++
 .../examples/fusion-plugin-auto-label/package.json |  2 +-
 .../examples/fusion-plugin-ci-status/CHANGELOG.md  |  8 ++
 .../examples/fusion-plugin-ci-status/package.json  |  2 +-
 .../fusion-plugin-notification/CHANGELOG.md        |  8 ++
 .../fusion-plugin-notification/package.json        |  2 +-
 .../fusion-plugin-settings-demo/CHANGELOG.md       |  8 ++
 .../fusion-plugin-settings-demo/package.json       |  2 +-
 plugins/fusion-plugin-hermes-runtime/CHANGELOG.md  |  8 ++
 plugins/fusion-plugin-hermes-runtime/package.json  |  2 +-
 .../fusion-plugin-openclaw-runtime/CHANGELOG.md    |  8 ++
 .../fusion-plugin-openclaw-runtime/package.json    |  2 +-
 .../fusion-plugin-paperclip-runtime/CHANGELOG.md   |  8 ++
 .../fusion-plugin-paperclip-runtime/package.json   |  2 +-
 78 files changed, 499 insertions(+), 115 deletions(-)

Fusion-Task-Id: FN-2923
2026-04-29 17:23:22 -07:00

480 lines
18 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { TodoView } from "../TodoView";
const mockUseTodoLists = vi.fn();
const mockCreateTask = vi.fn();
const mockFetchAgents = vi.fn();
vi.mock("../../hooks/useTodoLists", () => ({
useTodoLists: (...args: unknown[]) => mockUseTodoLists(...args),
}));
const mockConfirm = vi.fn();
vi.mock("../../hooks/useConfirm", () => ({
useConfirm: () => ({ confirm: mockConfirm }),
}));
vi.mock("../../api", () => ({
createTask: (...args: unknown[]) => mockCreateTask(...args),
fetchAgents: (...args: unknown[]) => mockFetchAgents(...args),
}));
vi.mock("lucide-react", () => ({
Plus: () => <span data-testid="icon-plus" />,
Trash2: () => <span data-testid="icon-trash" />,
Pencil: () => <span data-testid="icon-pencil" />,
Check: () => <span data-testid="icon-check" />,
X: () => <span data-testid="icon-x" />,
ChevronUp: () => <span data-testid="icon-chevron-up" />,
ChevronDown: () => <span data-testid="icon-chevron-down" />,
Loader2: () => <span data-testid="icon-loader" />,
ListChecks: () => <span data-testid="icon-list-checks" />,
Bot: () => <span data-testid="icon-bot" />,
PlusCircle: () => <span data-testid="icon-plus-circle" />,
}));
function createMockTodoLists(overrides: Record<string, unknown> = {}) {
return {
lists: [
{ id: "list-1", title: "My List", createdAt: "2026-04-25T00:00:00.000Z" },
{ id: "list-2", title: "Work Tasks", createdAt: "2026-04-25T00:00:00.000Z" },
],
items: [
{ id: "item-1", listId: "list-1", text: "Buy groceries", completed: false, sortOrder: 0 },
{ id: "item-2", listId: "list-1", text: "Clean house", completed: true, sortOrder: 1 },
{ id: "item-3", listId: "list-2", text: "Write report", completed: false, sortOrder: 0 },
],
loading: false,
error: null,
selectedListId: "list-1",
setSelectedListId: vi.fn(),
createList: vi.fn().mockResolvedValue(undefined),
renameList: vi.fn().mockResolvedValue(undefined),
deleteList: vi.fn().mockResolvedValue(undefined),
createItem: vi.fn().mockResolvedValue(undefined),
updateItem: vi.fn().mockResolvedValue(undefined),
toggleItem: vi.fn().mockResolvedValue(undefined),
deleteItem: vi.fn().mockResolvedValue(undefined),
reorderItems: vi.fn().mockResolvedValue(undefined),
...overrides,
};
}
describe("TodoView", () => {
const addToast = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
mockConfirm.mockReset();
mockConfirm.mockResolvedValue(true);
mockCreateTask.mockResolvedValue({ id: "FN-999" });
mockFetchAgents.mockResolvedValue([
{ id: "agent-1", name: "Builder", role: "engineer", state: "active" },
{ id: "agent-2", name: "Terminated", role: "reviewer", state: "terminated" },
]);
mockUseTodoLists.mockReturnValue(createMockTodoLists());
});
it("renders header with Todos heading", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByRole("heading", { name: "Todos" })).toBeInTheDocument();
});
it("renders sidebar with list names", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByTestId("todo-list-list-1")).toHaveTextContent("My List");
expect(screen.getByTestId("todo-list-list-2")).toHaveTextContent("Work Tasks");
});
it("renders only items for the selected list", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByText("Buy groceries")).toBeInTheDocument();
expect(screen.getByText("Clean house")).toBeInTheDocument();
expect(screen.queryByText("Write report")).not.toBeInTheDocument();
});
it("shows loading spinner when loading is true", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ loading: true }));
render(<TodoView addToast={addToast} />);
expect(screen.getByText("Loading todos...")).toBeInTheDocument();
expect(screen.getByTestId("icon-loader")).toBeInTheDocument();
});
it("shows error message when error is set", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ error: "Something went wrong" }));
render(<TodoView addToast={addToast} />);
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
});
it("shows empty state when no lists exist", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ lists: [], items: [], selectedListId: null }));
render(<TodoView addToast={addToast} />);
expect(screen.getByText("No todo lists yet. Create one to get started.")).toBeInTheDocument();
});
it("shows empty state when selected list has no items", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ items: [] }));
render(<TodoView addToast={addToast} />);
expect(screen.getByText("No items in this list. Add one above.")).toBeInTheDocument();
});
it("shows select-list empty state when no list is selected", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ selectedListId: null }));
render(<TodoView addToast={addToast} />);
expect(screen.getByText("Select a list from the sidebar")).toBeInTheDocument();
});
it("clicking a list item calls setSelectedListId", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("todo-list-list-2"));
expect(state.setSelectedListId).toHaveBeenCalledWith("list-2");
});
it.each([
{ key: "Enter", shouldCreate: true },
{ key: "Escape", shouldCreate: false },
])("new list input keyboard behavior: $key", ({ key, shouldCreate }) => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("add-list-button"));
const input = screen.getByTestId("new-list-input");
fireEvent.change(input, { target: { value: "Weekend" } });
fireEvent.keyDown(input, { key });
if (shouldCreate) {
expect(state.createList).toHaveBeenCalledWith("Weekend");
} else {
expect(state.createList).not.toHaveBeenCalled();
}
});
it.each([
{ key: "Enter", shouldRename: true },
{ key: "Escape", shouldRename: false },
])("list rename keyboard behavior: $key", ({ key, shouldRename }) => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("rename-list-button-list-1"));
const input = screen.getByTestId("rename-list-input-list-1");
fireEvent.change(input, { target: { value: "Renamed List" } });
fireEvent.keyDown(input, { key });
if (shouldRename) {
expect(state.renameList).toHaveBeenCalledWith("list-1", "Renamed List");
} else {
expect(state.renameList).not.toHaveBeenCalled();
}
});
it("clicking trash icon on list calls deleteList", async () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("delete-list-button-list-1"));
await waitFor(() => {
expect(state.deleteList).toHaveBeenCalledWith("list-1");
});
expect(mockConfirm).toHaveBeenCalledWith({
title: "Delete List",
message: "Delete this list and all its items?",
danger: true,
});
});
it("does not delete a list when confirmation is canceled", async () => {
mockConfirm.mockResolvedValueOnce(false);
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("delete-list-button-list-1"));
await waitFor(() => {
expect(mockConfirm).toHaveBeenCalled();
});
expect(state.deleteList).not.toHaveBeenCalled();
});
it("typing add-item input and pressing Enter calls createItem", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
const input = screen.getByTestId("new-item-input");
fireEvent.change(input, { target: { value: "Pack bags" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(state.createItem).toHaveBeenCalledWith("Pack bags");
});
it("pressing Escape in add-item input clears the pending draft", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
const input = screen.getByTestId("new-item-input") as HTMLInputElement;
fireEvent.change(input, { target: { value: "Pack bags" } });
fireEvent.keyDown(input, { key: "Escape" });
expect(state.createItem).not.toHaveBeenCalled();
expect(input.value).toBe("");
});
it("clicking checkbox calls toggleItem with item ID", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("toggle-item-item-1"));
expect(state.toggleItem).toHaveBeenCalledWith("item-1");
});
it("completed items have strikethrough class", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByText("Clean house")).toHaveClass("todo-item-text--completed");
});
it.each([
{ key: "Enter", shouldSave: true },
{ key: "Escape", shouldSave: false },
])("item edit keyboard behavior: $key", ({ key, shouldSave }) => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByText("Buy groceries"));
const input = screen.getByTestId("edit-item-input-item-1");
fireEvent.change(input, { target: { value: "Buy vegetables" } });
fireEvent.keyDown(input, { key });
if (shouldSave) {
expect(state.updateItem).toHaveBeenCalledWith("item-1", { text: "Buy vegetables" });
} else {
expect(state.updateItem).not.toHaveBeenCalled();
}
});
it.each([
{ testId: "move-down-item-1", expected: ["item-2", "item-1"], message: "move down" },
{ testId: "move-up-item-2", expected: ["item-2", "item-1"], message: "move up" },
])("clicking reorder button ($message) calls reorderItems", ({ testId, expected }) => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId(testId));
expect(state.reorderItems).toHaveBeenCalledWith(expected);
});
it("disables boundary reorder controls for first and last items", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByRole("button", { name: "Move Buy groceries up" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Move Clean house down" })).toBeDisabled();
});
it("clicking trash icon on item calls deleteItem", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("delete-item-item-1"));
expect(state.deleteItem).toHaveBeenCalledWith("item-1");
});
it("trims surrounding whitespace before creating a list", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("add-list-button"));
const input = screen.getByTestId("new-list-input");
fireEvent.change(input, { target: { value: " Weekend " } });
fireEvent.keyDown(input, { key: "Enter" });
expect(state.createList).toHaveBeenCalledWith("Weekend");
});
it("trims surrounding whitespace before creating an item", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
const input = screen.getByTestId("new-item-input");
fireEvent.change(input, { target: { value: " Pack bags " } });
fireEvent.keyDown(input, { key: "Enter" });
expect(state.createItem).toHaveBeenCalledWith("Pack bags");
});
it("trims surrounding whitespace before renaming a list", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("rename-list-button-list-1"));
const input = screen.getByTestId("rename-list-input-list-1");
fireEvent.change(input, { target: { value: " Renamed List " } });
fireEvent.keyDown(input, { key: "Enter" });
expect(state.renameList).toHaveBeenCalledWith("list-1", "Renamed List");
});
it("trims surrounding whitespace before saving an item edit", () => {
const state = createMockTodoLists();
mockUseTodoLists.mockReturnValue(state);
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByText("Buy groceries"));
const input = screen.getByTestId("edit-item-input-item-1");
fireEvent.change(input, { target: { value: " Buy vegetables " } });
fireEvent.keyDown(input, { key: "Enter" });
expect(state.updateItem).toHaveBeenCalledWith("item-1", { text: "Buy vegetables" });
});
it("clicking another list cancels an in-progress list rename", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists());
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("rename-list-button-list-1"));
expect(screen.getByTestId("rename-list-input-list-1")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("todo-list-list-2"));
expect(screen.queryByTestId("rename-list-input-list-1")).not.toBeInTheDocument();
});
it("clears inline item edit when selected list changes", () => {
const initialState = createMockTodoLists({ selectedListId: "list-1" });
mockUseTodoLists.mockReturnValue(initialState);
const { rerender } = render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByText("Buy groceries"));
expect(screen.getByTestId("edit-item-input-item-1")).toBeInTheDocument();
mockUseTodoLists.mockReturnValue(createMockTodoLists({ selectedListId: "list-2" }));
rerender(<TodoView addToast={addToast} />);
expect(screen.queryByTestId("edit-item-input-item-1")).not.toBeInTheDocument();
});
it("shows new list input and hides empty-state prompt when create list is clicked", () => {
mockUseTodoLists.mockReturnValue(createMockTodoLists({ lists: [], items: [], selectedListId: null }));
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByRole("button", { name: "Create List" }));
expect(screen.getByTestId("new-list-input")).toBeInTheDocument();
expect(screen.queryByText("No todo lists yet. Create one to get started.")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Create List" })).not.toBeInTheDocument();
});
it("marks the selected list with aria-current", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByTestId("todo-list-list-1")).toHaveAttribute("aria-current", "true");
expect(screen.getByTestId("todo-list-list-2")).not.toHaveAttribute("aria-current", "true");
});
it("applies active class to selected list", () => {
render(<TodoView addToast={addToast} />);
const selectedButton = screen.getByTestId("todo-list-list-1");
expect(selectedButton.closest(".todo-list-item")).toHaveClass("todo-list-item--active");
});
it("Create Task button renders for each todo item", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByTestId("create-task-from-item-1")).toBeInTheDocument();
expect(screen.getByTestId("create-task-from-item-2")).toBeInTheDocument();
});
it("Assign to Agent button renders for each todo item", () => {
render(<TodoView addToast={addToast} />);
expect(screen.getByTestId("assign-agent-for-item-1")).toBeInTheDocument();
expect(screen.getByTestId("assign-agent-for-item-2")).toBeInTheDocument();
});
it("clicking Create Task button calls createTask with item text", async () => {
mockCreateTask.mockResolvedValueOnce({ id: "FN-123" });
render(<TodoView addToast={addToast} projectId="project-1" />);
fireEvent.click(screen.getByTestId("create-task-from-item-1"));
await waitFor(() => {
expect(mockCreateTask).toHaveBeenCalledWith(
{ description: "Buy groceries", column: "triage", source: { sourceType: "dashboard_ui" } },
"project-1",
);
});
expect(addToast).toHaveBeenCalledWith("Created FN-123 from todo", "success");
});
it("clicking Assign to Agent button opens agent picker", async () => {
render(<TodoView addToast={addToast} projectId="project-1" />);
fireEvent.click(screen.getByTestId("assign-agent-for-item-1"));
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenCalledWith(undefined, "project-1");
});
expect(screen.getByText("Builder")).toBeInTheDocument();
expect(screen.queryByText("Terminated")).not.toBeInTheDocument();
});
it("selecting an agent creates task assigned to that agent", async () => {
mockCreateTask.mockResolvedValueOnce({ id: "FN-234" });
render(<TodoView addToast={addToast} projectId="project-1" />);
fireEvent.click(screen.getByTestId("assign-agent-for-item-1"));
const agentButton = await screen.findByRole("button", { name: /Builder/i });
fireEvent.click(agentButton);
await waitFor(() => {
expect(mockCreateTask).toHaveBeenCalledWith(
{ description: "Buy groceries", column: "triage", assignedAgentId: "agent-1", source: { sourceType: "dashboard_ui" } },
"project-1",
);
});
expect(addToast).toHaveBeenCalledWith("Created FN-234 and assigned to Builder", "success");
});
it("error handling shows error toast", async () => {
mockCreateTask.mockRejectedValueOnce(new Error("boom"));
render(<TodoView addToast={addToast} />);
fireEvent.click(screen.getByTestId("create-task-from-item-1"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Failed to create task: boom", "error");
});
});
});