// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The whole app, running, for the end-to-end suite: a Postgres with the three roles a linked managed // database exposes, the fake platform of @conusai-kit/testing (Blob, OCR jobs, audit, usage), the // migrate job, the worker and the web shell -- the same four things compose.yaml runs, as processes // on this machine so the suite needs no image build. // // Postgres is the one thing that needs Docker. The web shell runs under `next dev`, because // @conusai-kit/auth refuses the password-free dev sign-in the suite uses in a production start // (AUTH_DEV_LOGIN with NODE_ENV=production fails at boot, spec ยง7), which is the right refusal and // not one to work around. import { type ChildProcess, execFileSync, spawn } from "node:child_process" import { randomBytes } from "node:crypto" import { mkdirSync, openSync, readFileSync, rmSync } from "node:fs" import { join } from "node:path" import { startFakePlatform } from "@conusai-kit/testing/fake-platform" import { deterministicModel } from "../../test/support/fake-model" export const APP_ROOT = new URL("../..", import.meta.url).pathname const LOGS = join(APP_ROOT, "e2e", ".logs") export const PORTS = { web: Number(process.env.E2E_WEB_PORT ?? 3100), postgres: Number(process.env.E2E_POSTGRES_PORT ?? 55432), } export interface Stack { readonly baseUrl: string readonly platformUrl: string /** The fake platform, so a spec can change what its OCR reads. */ readonly platform: Awaited> stop(): Promise } const url = (role: string): string => `postgresql://appdev_${role}:appdev_${role}@127.0.0.1:${PORTS.postgres}/appdev` function sh(command: string, args: string[]): string { return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim() } async function until(label: string, check: () => Promise | boolean, seconds = 120): Promise { const deadline = Date.now() + seconds * 1000 for (;;) { try { if (await check()) return } catch { // not yet } if (Date.now() > deadline) throw new Error(`e2e: ${label} did not come up in ${seconds}s (logs in e2e/.logs)`) await new Promise((resolve) => setTimeout(resolve, 500)) } } function spawnLogged(name: string, command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): ChildProcess { mkdirSync(LOGS, { recursive: true }) const out = openSync(join(LOGS, `${name}.log`), "w") // A process group of its own, so stopping it stops what it started (next dev forks). return spawn(command, args, { cwd, env, stdio: ["ignore", out, out], detached: true }) } function stopGroup(child: ChildProcess): void { if (child.pid === undefined) return try { process.kill(-child.pid, "SIGTERM") } catch { // already gone } } export async function startStack(document: Record): Promise { const processes: ChildProcess[] = [] const name = `e2e-postgres-${randomBytes(4).toString("hex")}` const platform = await startFakePlatform() platform.ocr.setDocument(document) // The model is the deterministic reader of the unit tests: the suite proves the app around a model, not a model. platform.ai.respond(deterministicModel) sh("docker", [ "run", "-d", "--rm", "--name", name, "-p", `127.0.0.1:${PORTS.postgres}:5432`, "-e", "POSTGRES_PASSWORD=postgres", "-e", "POSTGRES_DB=appdev", "-v", `${join(APP_ROOT, "dev", "postgres-roles.sql")}:/docker-entrypoint-initdb.d/10-roles.sql:ro`, // pgvector: the run chooses the pgvector store (KNOWLEDGE_VECTOR_STORE below), whose table the // migrate job creates in this Postgres. "pgvector/pgvector:pg18", ]) const stopPostgres = (): void => { try { sh("docker", ["rm", "-f", name]) } catch { // already gone } } try { // The init script creates the roles after the server first starts, then it restarts: "ready" // is the app's own login working, not pg_isready. await until("postgres", () => { sh("docker", ["exec", name, "psql", "-U", "appdev_app", "-h", "127.0.0.1", "-d", "appdev", "-c", "select 1"]) return true }) const common: NodeJS.ProcessEnv = { ...process.env, // The fake platform is a development and test switch the app accepts in these modes alone (config/src/env.ts). NODE_ENV: "development", CONUSAI_FAKE_PLATFORM: "1", // What the platform injects when the environment has OTLP: the web shell sends its logs and spans to // the fake platform, where a spec reads them back with the request's trace id. OTEL_EXPORTER_OTLP_ENDPOINT: `${platform.url}/otel`, CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "e2e-token", CONUSAI_PROJECT_ID: "42", // What the platform injects for usage metering (AT4.1): the fake platform keeps the usage of // this organization beside the audit events, which is where a spec reads both. CONUSAI_ORG_ID: "e2e", CONUSAI_PUBLIC_URL: `http://127.0.0.1:${PORTS.web}`, CONUSAI_RELEASE: "e2e", POSTGRES_URL: url("app"), POSTGRES_READER_URL: url("reader"), KNOWLEDGE_VECTOR_STORE: "pgvector", AUTH_SESSION_SECRET: randomBytes(32).toString("base64url"), } delete common.POSTGRES_MIGRATE_URL // The migrate job holds the owner's URL and nothing else does. execFileSync("node", ["apps/worker/dist/migrate.js"], { cwd: APP_ROOT, env: { ...common, POSTGRES_MIGRATE_URL: url("owner") }, stdio: "inherit", }) // The demo data, written with the kit by the same script `pnpm dev` runs: the specs that look at it // are proof it works, and the others each make their own organization beside it. execFileSync("node", ["apps/worker/dist/seed-main.js"], { cwd: APP_ROOT, env: common, stdio: "inherit" }) // A dev server killed mid-write leaves a cache Turbopack panics on at the next start: begin clean. rmSync(join(APP_ROOT, "apps", "web", ".next"), { recursive: true, force: true }) processes.push( spawnLogged("worker", "node", ["apps/worker/dist/main.js"], APP_ROOT, { ...common, WORKER_HEARTBEAT_FILE: join(LOGS, "worker-heartbeat"), }), ) processes.push( spawnLogged( "web", process.execPath, ["node_modules/next/dist/bin/next", "dev", "-p", String(PORTS.web), "-H", "127.0.0.1"], join(APP_ROOT, "apps", "web"), { ...common, AUTH_DEV_LOGIN: "1", AUTH_INSECURE_COOKIES: "1", }, ), ) const baseUrl = `http://127.0.0.1:${PORTS.web}` await until("the web shell", async () => (await fetch(`${baseUrl}/api/ready`)).ok, 180) await until("the worker", () => readFileSync(join(LOGS, "worker.log"), "utf8").includes("worker: serving"), 60) return { baseUrl, platformUrl: platform.url, platform, async stop() { for (const child of processes) stopGroup(child) await platform.close() stopPostgres() }, } } catch (error) { for (const child of processes) stopGroup(child) await platform.close() stopPostgres() throw error } }