- types.ts: consolidate NoteDoc, WorkspaceDoc, NoteAgentActionDoc etc. from client files - notes-client.ts: import from types.ts, optimize getNoteDetail with direct GET /notes/:id - review-client.ts: import from types.ts, use /note-agent-actions/pending (eliminates N+1) - notes-client.ts: use /workspaces/summaries (eliminates fetch-all-notes for counts) - backend: add GET /workspaces/summaries with noteCount per workspace - backend: add GET /note-agent-actions/pending (cross-workspace) - backend: add countNotesByWorkspaces + listPendingActions repository functions - Add createNote, archiveNote, restoreNote, createNoteRelationship client functions - Fix existing tests for new route counts and mock order
165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
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();
|
||
});
|
||
});
|