import { describe, expect, it, vi, beforeEach } from "vitest"; const fetchMock = vi.fn(); const extractSuggestedTasksMock = vi.fn(); vi.mock("@bytelyst/api-client", () => ({ createApiClient: () => ({ fetch: fetchMock, }), })); vi.mock("@/lib/extraction-client", () => ({ extractSuggestedTasks: (...args: unknown[]) => extractSuggestedTasksMock(...args), })); import { getNoteDetail } from "@/lib/notes-client"; describe("getNoteDetail", () => { beforeEach(() => { fetchMock.mockReset(); extractSuggestedTasksMock.mockReset(); }); it("merges backend tasks with extracted suggestions, preserves artifact blob metadata, and normalizes review state", async () => { const noteItem = { id: "note-1", workspaceId: "workspace-1", title: "Launch note", body: "Sarah agreed to handle the testing by Friday.", status: "active", tags: ["launch"], updatedAt: "2026-03-10T12:01:00.000Z", updatedBy: "editor-1", createdBy: "editor-1", sourceType: "manual", }; // 1. GET /notes (fallback path — no knownWorkspaceId) fetchMock.mockResolvedValueOnce({ items: [noteItem] }); // 2–6. Parallel: /workspaces, /notes?workspaceId=..., /note-tasks, /note-artifacts, /note-agent-actions fetchMock.mockResolvedValueOnce({ items: [ { id: "workspace-1", name: "Product", members: [{ userId: "owner-1", role: "owner" }], updatedAt: "2026-03-10T12:00:00.000Z", updatedBy: "owner-1", }, ], }); fetchMock.mockResolvedValueOnce({ items: [noteItem] }); fetchMock.mockResolvedValueOnce({ items: [ { id: "task-1", noteId: "note-1", title: "Review approval UX cut line", status: "open", source: "manual", }, ], }); fetchMock.mockResolvedValueOnce({ items: [ { id: "artifact-1", noteId: "note-1", artifactType: "file", title: "Launch brief.pdf", description: "Ready for review", blobPath: "notelett/user-1/launch-brief.pdf", contentType: "application/pdf", sizeBytes: 2048, }, ], }); fetchMock.mockResolvedValueOnce({ items: [ { id: "action-1", noteId: "note-1", actorId: "agent-1", actorType: "agent", actionType: "summarize", state: "draft", afterSummary: "Drafted a summary update.", updatedAt: "2026-03-10T12:03:00.000Z", }, { id: "action-2", noteId: "note-1", actorId: "agent-2", actorType: "agent", actionType: "extract_tasks", state: "approved", afterSummary: "Approved task extraction.", updatedAt: "2026-03-10T12:02:00.000Z", }, ], }); // 7. GET /note-relationships (sequential after parallel batch) fetchMock.mockResolvedValueOnce({ items: [] }); extractSuggestedTasksMock.mockResolvedValue([ { id: "extract-review-0", title: "Review approval UX cut line", status: "todo", source: "agent", }, { id: "extract-test-1", title: "Sarah agreed to handle the testing", status: "todo", source: "agent", }, ]); const note = await getNoteDetail("note-1"); expect(note).not.toBeNull(); expect(extractSuggestedTasksMock).toHaveBeenCalledWith( "Sarah agreed to handle the testing by Friday." ); expect(note?.metadata.reviewState).toBe("none"); expect(note?.tasks).toEqual([ { id: "task-1", title: "Review approval UX cut line", status: "todo", source: "manual", }, { id: "extract-test-1", title: "Sarah agreed to handle the testing", status: "todo", source: "agent", }, ]); expect(note?.artifacts).toEqual([ { id: "artifact-1", name: "Launch brief.pdf", type: "file", status: "ready", blobPath: "notelett/user-1/launch-brief.pdf", contentType: "application/pdf", sizeBytes: 2048, }, ]); expect(note?.timeline[0]?.status).toBe("draft"); }); it("returns null when the note is missing", async () => { fetchMock.mockResolvedValueOnce({ items: [] }); fetchMock.mockResolvedValueOnce({ items: [] }); const note = await getNoteDetail("missing-note"); expect(note).toBeNull(); expect(extractSuggestedTasksMock).not.toHaveBeenCalled(); }); });