Files
fusion/packages/dashboard/app/components/__tests__/MailboxTaskRecommendations.test.tsx
gsxdsm 872d260ab8 FN-9104: Add mailbox recommendation task actions
Add guarded task creation directly to completed-task recommendation notices.

- Render live recommendation details and task actions in mailbox detail and conversation surfaces.
- Prevent duplicate creates, expose existing task links, and support unavailable and retry states.
- Cover desktop and mobile surfaces, update notice copy and documentation, and add a release changeset.

Files changed:
 .../fn-9104-mailbox-recommendation-create.md       |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 .../__tests__/task-recommendation-notice.test.ts   |   7 ++
 packages/core/src/task-recommendation-notice.ts    |   9 +-
 packages/dashboard/app/components/MailboxModal.tsx |   3 +
 .../app/components/MailboxTaskRecommendations.css  |   9 ++
 .../app/components/MailboxTaskRecommendations.tsx  | 127 +++++++++++++++++++++
 packages/dashboard/app/components/MailboxView.tsx  |   3 +
 .../MailboxTaskRecommendations.surfaces.test.tsx   |  77 +++++++++++++
 .../__tests__/MailboxTaskRecommendations.test.tsx  |  72 ++++++++++++
 10 files changed, 311 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-9104

Fusion-Task-Lineage: 94f53bfe-f57f-42ee-970e-56658129c1e0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 17:36:32 -07:00

73 lines
4.3 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { MessageMetadata } from "@fusion/core";
import { describe, expect, it, beforeEach, vi } from "vitest";
import { createTaskFromRecommendation, fetchTaskDetail } from "../../api";
import { MailboxTaskRecommendations } from "../MailboxTaskRecommendations";
vi.mock("../../api", () => ({ createTaskFromRecommendation: vi.fn(), fetchTaskDetail: vi.fn() }));
const metadata: MessageMetadata = { kind: "task-recommendation-notice", taskId: "FN-9100", recommendationIds: ["recommendation-1"] };
const detail = { id: "FN-9100", recommendations: [{ id: "recommendation-1", title: "Follow up", description: "Finish the optional work.", category: "feature" }] };
describe("MailboxTaskRecommendations", () => {
beforeEach(() => vi.resetAllMocks());
it("renders nothing for non-notices, missing parents, and empty recommendation ids", () => {
for (const candidate of [{}, { kind: "task-recommendation-notice", recommendationIds: ["recommendation-1"] }, { kind: "task-recommendation-notice", taskId: "FN-9100", recommendationIds: [] }]) {
const { container, unmount } = render(<MailboxTaskRecommendations metadata={candidate} />);
expect(container).toBeEmptyDOMElement();
unmount();
}
});
it("keeps a failed parent lookup inert", async () => {
vi.mocked(fetchTaskDetail).mockRejectedValue(new Error("not found"));
render(<MailboxTaskRecommendations metadata={metadata} />);
await waitFor(() => expect(screen.getByTestId("mailbox-task-recommendations-unavailable")).toBeInTheDocument());
expect(screen.queryByRole("button", { name: "Create task" })).not.toBeInTheDocument();
});
it("creates once and replaces the action with the linked task", async () => {
const onOpenTask = vi.fn();
vi.mocked(fetchTaskDetail).mockResolvedValue(detail as never);
vi.mocked(createTaskFromRecommendation).mockResolvedValue({ task: { id: "FN-9101" }, parent: detail } as never);
render(<MailboxTaskRecommendations metadata={metadata} projectId="project-1" onOpenTask={onOpenTask} />);
await screen.findByRole("button", { name: "Create task" });
fireEvent.click(screen.getByRole("button", { name: "Create task" }));
await waitFor(() => expect(screen.getByRole("button", { name: "View task FN-9101" })).toBeInTheDocument());
expect(createTaskFromRecommendation).toHaveBeenCalledWith("FN-9100", "recommendation-1", "project-1");
fireEvent.click(screen.getByRole("button", { name: "View task FN-9101" }));
expect(onOpenTask).toHaveBeenCalledWith("FN-9101");
});
it("shows existing links without a duplicate Create action", async () => {
vi.mocked(fetchTaskDetail).mockResolvedValue({ ...detail, recommendations: [{ ...detail.recommendations[0], createdTaskId: "FN-9101" }] } as never);
render(<MailboxTaskRecommendations metadata={metadata} />);
expect(await screen.findByRole("button", { name: "View task FN-9101" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Create task" })).not.toBeInTheDocument();
});
it("guards rapid duplicate clicks", async () => {
vi.mocked(fetchTaskDetail).mockResolvedValue(detail as never);
let resolveCreate!: (value: never) => void;
vi.mocked(createTaskFromRecommendation).mockImplementation(() => new Promise((resolve) => { resolveCreate = resolve; }));
render(<MailboxTaskRecommendations metadata={metadata} />);
await screen.findByRole("button", { name: "Create task" });
const button = screen.getByRole("button", { name: "Create task" });
fireEvent.click(button);
fireEvent.click(button);
expect(createTaskFromRecommendation).toHaveBeenCalledTimes(1);
resolveCreate({ task: { id: "FN-9101" }, parent: detail } as never);
await screen.findByRole("button", { name: "View task FN-9101" });
});
it("offers a retry after a rejected creation", async () => {
vi.mocked(fetchTaskDetail).mockResolvedValue(detail as never);
vi.mocked(createTaskFromRecommendation).mockRejectedValue(new Error("conflict"));
render(<MailboxTaskRecommendations metadata={metadata} />);
fireEvent.click(await screen.findByRole("button", { name: "Create task" }));
expect(await screen.findByRole("button", { name: "Retry creating task" })).toBeInTheDocument();
expect(screen.getByText("Could not create task. Try again.")).toBeInTheDocument();
});
});