learning_ai_notes/web/src/lib/notes-client.test.ts
saravanakumardb1 ee586065dd refactor(web+backend): consolidate types, optimize N+1 queries [D1, A3, A4, D2]
- 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
2026-03-19 07:32:54 -07:00

165 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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] });
// 26. 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();
});
});