test(web): cover integrated note runtime
This commit is contained in:
parent
b4634e9367
commit
5f3b32bb93
@ -2,6 +2,8 @@ import { render, screen } from "@testing-library/react";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import SearchPage from "./page";
|
import SearchPage from "./page";
|
||||||
|
|
||||||
|
const listNoteSummariesMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("next/link", () => ({
|
vi.mock("next/link", () => ({
|
||||||
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
|
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
|
||||||
<a href={href} {...props}>
|
<a href={href} {...props}>
|
||||||
@ -10,15 +12,32 @@ vi.mock("next/link", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/notes-client", () => ({
|
||||||
|
listNoteSummaries: () => listNoteSummariesMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("SearchPage", () => {
|
describe("SearchPage", () => {
|
||||||
it("renders an accessible search field, saved searches, and note links", () => {
|
it("renders an accessible search field, saved searches, and note links", async () => {
|
||||||
|
listNoteSummariesMock.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "note-prd-cutline",
|
||||||
|
workspaceId: "workspace-product",
|
||||||
|
title: "MVP cut line for agentic notes launch",
|
||||||
|
excerpt: "Define which note, task, search, and approval flows must exist before wider rollout.",
|
||||||
|
status: "active",
|
||||||
|
tags: ["mvp", "launch", "scope"],
|
||||||
|
updatedAt: "2026-03-10T14:30:00.000Z",
|
||||||
|
updatedBy: "Product Lead",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
render(<SearchPage />);
|
render(<SearchPage />);
|
||||||
|
|
||||||
expect(screen.getByRole("heading", { level: 1, name: "Search" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { level: 1, name: "Search" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("textbox", { name: "Search notes" })).toBeInTheDocument();
|
expect(screen.getByRole("textbox", { name: "Search notes" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Saved searches")).toBeInTheDocument();
|
expect(screen.getByText("Saved searches")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Launch readiness")).toBeInTheDocument();
|
expect(screen.getByText("Launch readiness")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"/notes/note-prd-cutline"
|
"/notes/note-prd-cutline"
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import { render, screen } from "@testing-library/react";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import WorkspacesPage from "./page";
|
import WorkspacesPage from "./page";
|
||||||
|
|
||||||
|
const listNoteSummariesMock = vi.fn();
|
||||||
|
const listWorkspaceSummariesMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("next/link", () => ({
|
vi.mock("next/link", () => ({
|
||||||
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
|
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
|
||||||
<a href={href} {...props}>
|
<a href={href} {...props}>
|
||||||
@ -10,15 +13,45 @@ vi.mock("next/link", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/notes-client", () => ({
|
||||||
|
listNoteSummaries: () => listNoteSummariesMock(),
|
||||||
|
listWorkspaceSummaries: () => listWorkspaceSummariesMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("WorkspacesPage", () => {
|
describe("WorkspacesPage", () => {
|
||||||
it("renders an accessible workspace filter and saved workspace views", () => {
|
it("renders an accessible workspace filter and saved workspace views", async () => {
|
||||||
|
listNoteSummariesMock.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "note-prd-cutline",
|
||||||
|
workspaceId: "workspace-product",
|
||||||
|
title: "MVP cut line for agentic notes launch",
|
||||||
|
excerpt: "Define which note, task, search, and approval flows must exist before wider rollout.",
|
||||||
|
status: "active",
|
||||||
|
tags: ["mvp", "launch", "scope"],
|
||||||
|
updatedAt: "2026-03-10T14:30:00.000Z",
|
||||||
|
updatedBy: "Product Lead",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
listWorkspaceSummariesMock.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "workspace-product",
|
||||||
|
name: "Product Strategy",
|
||||||
|
description: "PRDs, roadmap cuts, launch tradeoffs, and operating decisions.",
|
||||||
|
owner: "Product Lead",
|
||||||
|
noteCount: 1,
|
||||||
|
visibility: "shared",
|
||||||
|
updatedAt: "2026-03-10T14:35:00.000Z",
|
||||||
|
tags: ["strategy", "launch", "roadmap"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
render(<WorkspacesPage />);
|
render(<WorkspacesPage />);
|
||||||
|
|
||||||
expect(screen.getByRole("heading", { level: 1, name: "Workspaces" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { level: 1, name: "Workspaces" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("textbox", { name: "Filter workspaces" })).toBeInTheDocument();
|
expect(screen.getByRole("textbox", { name: "Filter workspaces" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Saved views")).toBeInTheDocument();
|
expect(screen.getByText("Saved views")).toBeInTheDocument();
|
||||||
expect(screen.getByText("All workspaces")).toBeInTheDocument();
|
expect(screen.getByText("All workspaces")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"/notes/note-prd-cutline"
|
"/notes/note-prd-cutline"
|
||||||
);
|
);
|
||||||
|
|||||||
161
web/src/lib/notes-client.test.ts
Normal file
161
web/src/lib/notes-client.test.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
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 () => {
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
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: "bytelyst-notes/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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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: "bytelyst-notes/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user