// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // biome-ignore-all lint/suspicious/noTemplateCurlyInString: a ${NAME} in this file is what the // compose document holds verbatim -- compose interpolates it from the deployment's env file -- so // the assertions compare against that text and not against a value. // compose.yaml: the ordering the platform does not give (decision AT-D14), and the keys the // platform's own compose model accepts. // // The model is crates/conusai-deployer/src/compose_model.rs: a service key it has no field for // fails the deployment's parse, and so does a host binding in `ports`. This suite is that list, // read from the document, so a key added here in a hurry fails in review and not on stage. import { readFileSync } from "node:fs" import { describe, expect, it } from "vitest" import { parse } from "yaml" interface Service { build?: { context: string; dockerfile: string; target?: string } image?: string command?: string[] environment?: Record expose?: (string | number)[] ports?: unknown[] depends_on?: Record restart?: string healthcheck?: { test: string[]; interval?: string } deploy?: { resources?: { limits?: { cpus?: string; memory?: string } } } init?: boolean working_dir?: string stop_grace_period?: string } const document = parse(readFileSync(new URL("../compose.yaml", import.meta.url), "utf8")) as { services: Record } const services = document.services const optional = (name: string): string => `\${${name}:-}` /** * The passthroughs every profile owes each process (BUG-APP-02): a variable the code reads but * compose never names never reaches the container, so the deployment's value silently loses to * the default. `${VAR}` where boot refuses to run without it, `${VAR:-}` where a default stands. */ function expectPassthroughs(named: Record): void { for (const name of [ "OIDC_WEB_CLIENT_SECRET", "OIDC_RESOURCE", "OIDC_INTROSPECTION_CLIENT_ID", "OIDC_INTROSPECTION_CLIENT_SECRET", "OIDC_RESOURCE_STATUS_URL", "OIDC_SCOPES", "AUTH_PROVIDER_GENERATION", "AUTH_COOKIE_NAME", "AUTH_SESSION_IDLE_SECONDS", "AUTH_SESSION_ABSOLUTE_SECONDS", ]) { expect(named.web?.environment?.[name], `web ${name}`).toBe(optional(name)) } for (const name of ["web", "worker"] as const) { if (named[name]) expect(named[name]?.environment?.CONUSAI_ORG_SLUG).toBe(optional("CONUSAI_ORG_SLUG")) } if (named.worker) { expect(named.worker.environment?.CONUSAI_PUBLIC_URL).toBe("${CONUSAI_PUBLIC_URL}") expect(named.worker.environment?.DOCUMENTS_EXCLUDED_PAGE_CLASSES).toBe(optional("DOCUMENTS_EXCLUDED_PAGE_CLASSES")) } if (named.migrate) expect(named.migrate.environment?.CONUSAI_RELEASE).toBe("${CONUSAI_RELEASE}") for (const name of [ "CONUSAI_ENVIRONMENT", "CONUSAI_DSN", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_SERVICE_NAME", "OTEL_SERVICE_VERSION", ]) { if (named.migrate) expect(named.migrate.environment?.[name], `migrate ${name}`).toBe(optional(name)) } } /** Every key TenantService models (compose_model.rs). Anything else fails the platform's parse. */ const MODELLED = new Set([ "image", "build", "command", "environment", "ports", "expose", "volumes", "depends_on", "restart", "healthcheck", "deploy", "init", "stop_grace_period", "stop_signal", "working_dir", "user", ]) describe("compose.yaml", () => { it("has the three services of conusai.json and nothing else", () => { expect(Object.keys(services).sort()).toEqual(["migrate", "web", "worker"]) }) it("uses only the keys the platform's compose model has a field for", () => { for (const [name, service] of Object.entries(services)) { for (const key of Object.keys(service)) { expect(MODELLED.has(key), `${name}.${key} is not in the platform's compose model`).toBe(true) } } }) it("builds every service from one Dockerfile, so one image serves all three", () => { const builds = Object.values(services).map((service) => JSON.stringify(service.build)) expect(new Set(builds).size).toBe(1) expect(Object.values(services).every((service) => service.image === undefined)).toBe(true) }) it("starts each process with its own argv, never a shell string", () => { for (const [name, service] of Object.entries(services)) { expect(Array.isArray(service.command), `${name} has no argv command`).toBe(true) } expect(services.migrate?.command?.at(-1)).toContain("migrate.js") }) it("runs the migrate job to completion before web and worker start", () => { // `kind: job` in conusai.json is inert at deploy time: one `docker compose up -d --wait` // starts everything at once, and `pre_deploy_command` has no executor at all. This is the // ordering (decision AT-D14). expect(services.migrate?.restart).toBe("no") for (const name of ["web", "worker"] as const) { expect(services[name]?.depends_on?.migrate).toEqual({ condition: "service_completed_successfully" }) } }) it("publishes no host port", () => { for (const service of Object.values(services)) expect(service.ports).toBeUndefined() expect(services.web?.expose).toEqual(["3000"]) }) it("gives every service a limit, and the two long-running ones a healthcheck", () => { for (const [name, service] of Object.entries(services)) { expect(service.deploy?.resources?.limits?.memory, `${name} has no memory limit`).toBeTruthy() expect(service.deploy?.resources?.limits?.cpus, `${name} has no cpu limit`).toBeTruthy() expect(service.init, `${name} does not reap its children`).toBe(true) } expect(services.web?.healthcheck?.test).toBeTruthy() expect(services.worker?.healthcheck?.test).toBeTruthy() // A job is not healthy or unhealthy; it succeeds or it fails. expect(services.migrate?.healthcheck).toBeUndefined() }) it("hands the owner's URL to the migrate job alone", () => { expect(services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBe("${POSTGRES_MIGRATE_URL}") expect(services.web?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined() expect(services.worker?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined() for (const name of ["web", "worker", "migrate"] as const) { expect(services[name]?.environment?.POSTGRES_URL).toBe("${POSTGRES_URL}") expect(services[name]?.environment?.POSTGRES_READER_URL).toBe("${POSTGRES_READER_URL}") } }) it("passes every variable each process reads through to its container", () => { expectPassthroughs(services) }) }) describe("profiles/mariadb/compose.yaml", () => { const profile = parse(readFileSync(new URL("../profiles/mariadb/compose.yaml", import.meta.url), "utf8")) as { services: Record } it("hands the one MariaDB login to every process and the Valkey queue to web and worker", () => { for (const name of ["web", "worker", "migrate"] as const) { expect(profile.services[name]?.environment?.MARIADB_URL).toBe("${MARIADB_URL}") } for (const name of ["web", "worker"] as const) { expect(profile.services[name]?.environment?.REDIS_URL).toBe("${REDIS_URL}") } // MariaDB has no row-level security: there is no owner/runtime/reader split to hand out. expect(profile.services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined() }) it("passes every variable each process reads through to its container", () => { expectPassthroughs(profile.services) }) }) for (const name of ["mongodb", "convex"] as const) { describe(`profiles/${name}/compose.yaml`, () => { const profile = parse(readFileSync(new URL(`../profiles/${name}/compose.yaml`, import.meta.url), "utf8")) as { services: Record } it("passes the platform billing slug and web auth settings through to the containers", () => { for (const service of ["web", "worker"] as const) { if (profile.services[service]) { expect(profile.services[service]?.environment?.CONUSAI_ORG_SLUG).toBe( name === "convex" ? `\${CONUSAI_ORG_SLUG}` : optional("CONUSAI_ORG_SLUG"), ) } } for (const variable of [ "OIDC_WEB_CLIENT_SECRET", "OIDC_RESOURCE", "OIDC_INTROSPECTION_CLIENT_ID", "OIDC_INTROSPECTION_CLIENT_SECRET", "OIDC_RESOURCE_STATUS_URL", "OIDC_SCOPES", "AUTH_PROVIDER_GENERATION", "AUTH_COOKIE_NAME", "AUTH_SESSION_IDLE_SECONDS", "AUTH_SESSION_ABSOLUTE_SECONDS", ]) { expect(profile.services.web?.environment?.[variable], `web ${variable}`).toBe(optional(variable)) } }) }) } describe("the local stack", () => { const dev = readFileSync(new URL("../compose.dev.yaml", import.meta.url), "utf8") const roles = readFileSync(new URL("../dev/postgres-roles.sql", import.meta.url), "utf8") const example = readFileSync(new URL("../.env.example", import.meta.url), "utf8") it("creates the three roles the platform creates, with the same powers", () => { for (const role of ["appdev_owner", "appdev_app", "appdev_reader"]) { expect(roles, `${role} is not created`).toMatch(new RegExp(`CREATE ROLE ${role}\\s+LOGIN`)) } // The three are LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION, // as data-services DS21 creates them. expect(roles.match(/NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION/g)).toHaveLength(3) // The owner owns schema public; the runtime role gets rows, never ownership. expect(roles).toContain('ALTER SCHEMA public OWNER TO "appdev_owner"') expect(roles).toContain('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "appdev_app"') // The reader gets no table privilege here: the app's own migrations grant it what it reads. expect(roles).not.toMatch(/GRANT SELECT ON (ALL )?TABLES? [^;]*"appdev_reader"/) }) it("hands each local process the same URL the platform would", () => { expect(example).toContain("POSTGRES_URL=postgresql://appdev_app:") expect(example).toContain("POSTGRES_READER_URL=postgresql://appdev_reader:") expect(example).toContain("POSTGRES_MIGRATE_URL=postgresql://appdev_owner:") }) it("keeps the development stack out of what is deployed", () => { // conusai.json names compose.yaml alone, so nothing in compose.dev.yaml is ever deployed. expect(dev).toContain("image: pgvector/pgvector:pg18") expect(readFileSync(new URL("../conusai.json", import.meta.url), "utf8")).not.toContain("compose.dev.yaml") }) })