import { expect, test, type Page } from "@playwright/test"; import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; const ADMIN_EMAIL = "admin@capakraken.dev"; const ADMIN_PASSWORD = "admin123"; const CURRENT_CONVERSATION_ID = "assistant-e2e-current"; const DB_WORKDIR = resolve(process.cwd(), "../../packages/db"); const WEB_ENV_PATHS = [ resolve(process.cwd(), ".env.local"), resolve(process.cwd(), "apps/web/.env.local"), ]; function resolveDatabaseUrl(): string { const explicitPlaywrightUrl = process.env["PLAYWRIGHT_DATABASE_URL"]; if (explicitPlaywrightUrl) { return explicitPlaywrightUrl; } for (const envPath of WEB_ENV_PATHS) { if (!existsSync(envPath)) { continue; } const envFile = readFileSync(envPath, "utf8"); for (const rawLine of envFile.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith("#")) { continue; } if (line.startsWith("PLAYWRIGHT_DATABASE_URL=")) { return line.slice("PLAYWRIGHT_DATABASE_URL=".length); } if (line.startsWith("DATABASE_URL_TEST=")) { return line.slice("DATABASE_URL_TEST=".length); } if (line.startsWith("DATABASE_URL=")) { return line.slice("DATABASE_URL=".length); } } } const fallbackTestUrl = process.env["DATABASE_URL_TEST"]; if (fallbackTestUrl) { return fallbackTestUrl; } const fallback = process.env["DATABASE_URL"]; if (fallback) { return fallback; } throw new Error("DATABASE_URL is not available for the Playwright assistant approval test."); } function runDbJson(body: string): T { const script = ` import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient({ datasources: { db: { url: process.env.DATABASE_URL, }, }, }); try { ${body} } finally { await prisma.$disconnect(); } `; const output = execFileSync("node", ["--input-type=module", "-e", script], { cwd: DB_WORKDIR, env: { ...process.env, DATABASE_URL: resolveDatabaseUrl(), }, encoding: "utf8", }).trim(); return JSON.parse(output) as T; } function runDb(body: string): void { runDbJson(`${body}\nconsole.log("null");`); } async function signInAsAdmin(page: Page) { await page.goto("/auth/signin"); await page.fill('input[type="email"]', ADMIN_EMAIL); await page.fill('input[type="password"]', ADMIN_PASSWORD); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); } test.describe("Assistant approvals", () => { test.describe.configure({ mode: "serial" }); test.beforeEach(async ({ page }) => { await page.addInitScript((conversationId) => { window.sessionStorage.setItem("capakraken-chat-conversation-id", conversationId); }, CURRENT_CONVERSATION_ID); runDb(` await prisma.systemSettings.upsert({ where: { id: "singleton" }, update: { aiProvider: "openai", azureOpenAiApiKey: "e2e-dummy-key", azureOpenAiDeployment: "gpt-4o-mini", }, create: { id: "singleton", aiProvider: "openai", azureOpenAiApiKey: "e2e-dummy-key", azureOpenAiDeployment: "gpt-4o-mini", }, }); const admin = await prisma.user.findUniqueOrThrow({ where: { email: ${JSON.stringify(ADMIN_EMAIL)} }, select: { id: true }, }); await prisma.assistantApproval.deleteMany({ where: { userId: admin.id }, }); await prisma.client.deleteMany({ where: { name: { startsWith: "E2E Approval Client", }, }, }); `); }); test.afterEach(async () => { runDb(` await prisma.assistantApproval.deleteMany({ where: { conversationId: { startsWith: "assistant-e2e-", }, }, }); await prisma.client.deleteMany({ where: { name: { startsWith: "E2E Approval Client", }, }, }); `); }); test("renders the pending approval inbox and handles cross-conversation actions", async ({ page }) => { const suffix = Date.now(); const currentClientName = `E2E Approval Client Current ${suffix}`; const otherClientName = `E2E Approval Client Other ${suffix}`; const otherConversationId = `assistant-e2e-other-${suffix}`; const { currentApproval, otherApproval } = runDbJson<{ currentApproval: { id: string; summary: string }; otherApproval: { id: string; summary: string }; }>(` const admin = await prisma.user.findUniqueOrThrow({ where: { email: ${JSON.stringify(ADMIN_EMAIL)} }, select: { id: true }, }); const currentApproval = await prisma.assistantApproval.create({ data: { userId: admin.id, conversationId: ${JSON.stringify(CURRENT_CONVERSATION_ID)}, toolName: "create_client", toolArguments: ${JSON.stringify(JSON.stringify({ name: currentClientName }))}, summary: ${JSON.stringify(`create client (name=${currentClientName})`)}, expiresAt: new Date(Date.now() + 15 * 60 * 1000), }, select: { id: true, summary: true }, }); const otherApproval = await prisma.assistantApproval.create({ data: { userId: admin.id, conversationId: ${JSON.stringify(otherConversationId)}, toolName: "create_client", toolArguments: ${JSON.stringify(JSON.stringify({ name: otherClientName }))}, summary: ${JSON.stringify(`create client (name=${otherClientName})`)}, expiresAt: new Date(Date.now() + 15 * 60 * 1000), }, select: { id: true, summary: true }, }); console.log(JSON.stringify({ currentApproval, otherApproval })); `); await signInAsAdmin(page); await page.goto("/dashboard"); await page.getByTestId("assistant-open-button").click(); await expect(page.getByRole("heading", { name: "HartBOT" })).toBeVisible(); await expect(page.getByTestId("assistant-open-approvals")).toBeVisible(); await expect(page.getByText(currentApproval.summary)).toBeVisible(); await expect(page.getByText(otherApproval.summary)).toBeVisible(); const currentCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="current"]').first(); const otherCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="other"]').first(); await expect(currentCard).toContainText("This chat"); await expect(otherCard).toContainText("Other chat"); await otherCard.getByTestId("assistant-approval-cancel").click(); await expect(page.getByText(`Aktion verworfen: ${otherApproval.summary}`)).toBeVisible(); await expect(page.locator(`[data-testid="assistant-approval-card"][data-approval-id="${otherApproval.id}"]`)).toHaveCount(0); await expect .poll(async () => { return runDbJson(` const approval = await prisma.assistantApproval.findUnique({ where: { id: ${JSON.stringify(otherApproval.id)} }, select: { status: true }, }); console.log(JSON.stringify(approval?.status ?? null)); `); }) .toBe("CANCELLED"); await currentCard.getByTestId("assistant-approval-confirm").click(); await expect(page.getByText(`Ausgeführt: Created client: ${currentClientName}`)).toBeVisible(); await expect(page.getByText(currentApproval.summary)).toBeVisible(); await expect .poll(async () => { return runDbJson(` const approval = await prisma.assistantApproval.findUnique({ where: { id: ${JSON.stringify(currentApproval.id)} }, select: { status: true }, }); console.log(JSON.stringify(approval?.status ?? null)); `); }) .toBe("APPROVED"); await expect .poll(async () => { return runDbJson<"created" | "missing">(` const client = await prisma.client.findFirst({ where: { name: ${JSON.stringify(currentClientName)} }, select: { id: true }, }); console.log(JSON.stringify(client ? "created" : "missing")); `); }) .toBe("created"); await expect(page.getByTestId("assistant-approval-card")).toHaveCount(0); }); });