1
31import { test, expect } from "../fixtures";
32import type { APIRequestContext, Page } from "@playwright/test";
33import type { PrismaClient } from "@prisma/client";
34
35const DSN = process.env.DATABASE_URL ?? "postgresql://factory:factory@localhost:5432/factory";
36
37async function newPrisma(): Promise<PrismaClient> {
38 const { PrismaClient: PC } = await import("@prisma/client");
39 return new PC({ datasources: { db: { url: DSN } } });
40}
41
42
43async function createPage(api: APIRequestContext, wsId: string, title: string): Promise<string> {
44 const res = await api.post(`/api/v1/workspaces/${wsId}/pages`, { data: { title } });
45 expect(res.status(), `createPage(${title})`).toBe(201);
46 return ((await res.json()) as { id: string }).id;
47}
48
49
51async function backdatePage(pageId: string, days: number): Promise<void> {
52 const prisma = await newPrisma();
53 try {
54 await prisma.page.update({
55 where: { id: pageId },
56 data: { updatedAt: new Date(Date.now() - days * 86_400_000) },
57 });
58 } finally {
59 await prisma.$disconnect();
60 }
61}
62
63
64async function pickFromPalette(page: Page, query: string, itemLabel: string): Promise<void> {
65 await page.getByTestId("command-palette-button").click();
66 const palette = page.getByRole("dialog", { name: "Command palette" });
67 await expect(palette).toBeVisible({ timeout: 10_000 });
68 const input = palette.getByRole("combobox");
69 await input.fill(query);
70
71
72
73 const option = palette.getByRole("option", { name: itemLabel });
74 await expect(option).toBeVisible({ timeout: 10_000 });
75 await option.click();
76}
77
78
80async function gotoPage(authPage: Page, ws: { id: string; slug: string }, pageId: string): Promise<void> {
81 await authPage.goto(`/w/${ws.slug}/page/${pageId}`);
82 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
83}
84
85test.describe("AI knowledge assistant (P6, T-172…T-175) — palette → assistant-dialog", () => {
86 test("extract-actions: notes → real tasks (dialog lists them, server persists them)", async ({
87 authPage,
88 api,
89 freshWorkspace
90 }) => {
91 const ws = await freshWorkspace("assistant-extract");
92 const probe = await createPage(api, ws.id, "probe");
93 await gotoPage(authPage, ws, probe);
94
95 await pickFromPalette(authPage, "extract action", "Extract action items (AI)");
96 const dialog = authPage.getByTestId("assistant-dialog");
97 await expect(dialog).toBeVisible({ timeout: 10_000 });
98 await expect(dialog.getByRole("heading", { name: "Extract action items" })).toBeVisible();
99
100
101 const input = dialog.getByTestId("assistant-input");
102 await expect(input).toBeVisible();
103 await expect(dialog.getByTestId("assistant-run")).toBeDisabled();
104
105 await input.fill("- Ship the whiteboard export\n- Review the Q3 budget");
106 await expect(dialog.getByTestId("assistant-run")).toBeEnabled();
107 await dialog.getByTestId("assistant-run").click();
108
109
110 await expect(dialog.getByTestId("assistant-result")).toContainText("Ship the whiteboard export", {
111 timeout: 20_000
112 });
113 await expect(dialog.getByTestId("assistant-result")).toContainText("Review the Q3 budget");
114
115
116
117 await expect
118 .poll(
119 async () => {
120 const res = await api.get(`/api/v1/workspaces/${ws.id}/tasks?scope=all`);
121 expect(res.status()).toBe(200);
122 const body = (await res.json()) as { data: Array<{ title: string }> };
123 const titles = body.data.map((t) => t.title);
124 return titles.includes("Ship the whiteboard export") && titles.includes("Review the Q3 budget");
125 },
126 { timeout: 15_000, message: "extract-actions created both tasks" }
127 )
128 .toBeTruthy();
129 });
130
131 test("find-stale: backdated pages are listed (90-day window)", async ({ authPage, api, freshWorkspace }) => {
132 const ws = await freshWorkspace("assistant-stale");
133 const a = "Stale Alpha Page";
134 const b = "Stale Beta Page";
135 const pa = await createPage(api, ws.id, a);
136 const pb = await createPage(api, ws.id, b);
137 await gotoPage(authPage, ws, pa);
138
139
140 await backdatePage(pa, 120);
141 await backdatePage(pb, 100);
142
143 await pickFromPalette(authPage, "find stale", "Find stale pages (AI)");
144 const dialog = authPage.getByTestId("assistant-dialog");
145 await expect(dialog).toBeVisible({ timeout: 10_000 });
146
147 await expect(dialog.getByLabel("Not updated in")).toHaveValue("90");
148
149 await dialog.getByTestId("assistant-run").click();
150 await expect(dialog.getByTestId("assistant-result")).toContainText("Stale Alpha Page", {
151 timeout: 20_000
152 });
153 await expect(dialog.getByTestId("assistant-result")).toContainText("Stale Beta Page");
154
155 await expect(dialog.getByTestId("assistant-result")).toContainText("Not updated in 90+ days");
156 });
157
158 test("find-duplicates: pages are grouped as likely duplicates", async ({ authPage, api, freshWorkspace }) => {
159 const ws = await freshWorkspace("assistant-dup");
160 const titles = ["Dup Page One", "Dup Page Two", "Dup Page Three", "Dup Page Four"];
161 const p0 = await createPage(api, ws.id, titles[0]);
162 for (let i = 1; i < titles.length; i++) await createPage(api, ws.id, titles[i]);
163 await gotoPage(authPage, ws, p0);
164
165 await pickFromPalette(authPage, "find duplicate", "Find duplicate pages (AI)");
166 const dialog = authPage.getByTestId("assistant-dialog");
167 await expect(dialog).toBeVisible({ timeout: 10_000 });
168
169 await dialog.getByTestId("assistant-run").click();
170
171
172 const result = dialog.getByTestId("assistant-result");
173 await expect(result).toContainText("Dup Page One", { timeout: 20_000 });
174 await expect(result).toContainText("Dup Page Four");
175 await expect(result.getByText(/Overlapping content \(mock\)/)).toHaveCount(2);
176 });
177});
178