266 lines
8.4 KiB
TypeScript
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);
|
|
});
|
|
});
|