Files
CapaKraken/apps/web/e2e/assistant-approvals.spec.ts

266 lines
8.4 KiB
TypeScript

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<T>(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<null>(`${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<string | null>(`
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<string | null>(`
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);
});
});