Files
Nexus/apps/web/e2e/assistant-approvals.spec.ts
T
Hartmut 01f8974314
CI / Architecture Guardrails (pull_request) Successful in 2m59s
CI / Typecheck (pull_request) Successful in 6m41s
CI / Lint (pull_request) Successful in 4m18s
CI / Assistant Split Regression (pull_request) Successful in 5m6s
CI / Unit Tests (pull_request) Successful in 7m21s
CI / Build (pull_request) Successful in 5m21s
CI / Fresh-Linux Docker Deploy (pull_request) Failing after 38s
CI / E2E Tests (pull_request) Successful in 3m28s
CI / Release Images (pull_request) Has been skipped
rename(phase 3): compose/DB/infra names + stray code refs capakraken → nexus
- docker-compose.yml / .prod.yml / .ci.yml: project names, POSTGRES_DB/USER,
  pg_isready, DATABASE_URL, volume names (nexus_pgdata, nexus_prod_*)
- .github/workflows/ci.yml: POSTGRES_PASSWORD, pg_isready, psql credentials,
  GRANT statements, POSTGRES_PASSWORD=nexus_dev for Docker Deploy job
- scripts/db-target-guard.mjs: expectedDatabase default, NEXUS_EXPECTED_DB_NAME
- scripts/prisma-with-env.mjs, e2e/test-server.mjs: env-var rename
- packages/db/src/safe-destructive-env.ts + reset-dispo-import.ts: DB name set
- packages/db/src/destructive-db-guard.ts: PROTECTED_DATABASE_NAMES → "nexus"
- packages/db/src/destructive-db-guard.test.ts: all fixture DB names + comments
- .env.example, tooling/deploy/deploy.env.example: DATABASE_URL, image refs
- packages/api: Redis channel/key prefixes (rbac-invalidate, sse, ratelimit),
  logger service name, app-base-url log prefix
- E2E: DB container names, localStorage/sessionStorage keys, email domains
- scripts: architecture-guardrails filter, export/import-dev-seed defaults,
  harden-postgres defaults, start.sh pg_isready, worktree-hygiene fixture
- tooling/migrate/rename-to-nexus.sh: new maintenance-window cutover script

Only intentional capakraken survivor: anonymization.ts DEFAULT_ANONYMIZATION_SEED
(functional cryptographic constant — changing it would invalidate stored aliases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:35:39 +02:00

276 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@nexus.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("nexus-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);
});
});