// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // conusai.json against the real parser's own schema (AT2.1 done_when). // // `schemas/conusai.schema.json` is generated from crates/conusai-presets, and // scripts/vendor/test-template.sh runs that generator's `--check` before this suite, so what is // validated here is the closed schema the platform parses with: an unknown key fails, and so // does a missing one. import { existsSync, readdirSync, readFileSync } from "node:fs" import { join } from "node:path" import { Ajv2020 } from "ajv/dist/2020.js" import addFormats from "ajv-formats" import { describe, expect, it } from "vitest" import { moduleSelected } from "../modules/inclusion.js" const repoRoot = process.env.CONUSAI_REPO_ROOT const config = JSON.parse(readFileSync(new URL("../conusai.json", import.meta.url), "utf8")) as { schema_version: number compose: { file: string } services: Record< string, { kind?: string pre_deploy_command?: string public?: { port: number; primary?: boolean } health_check?: { path: string } size?: string postgres?: { migrate?: boolean } } > links: { type: string }[] previews: { enabled: boolean; size?: string } token?: { permissions: string[] } guides?: { bundle: string } } describe("conusai.json", () => { it.runIf(repoRoot)("validates against the generated conusai.json schema", () => { const schema = JSON.parse(readFileSync(join(repoRoot as string, "schemas/conusai.schema.json"), "utf8")) as object const ajv = new Ajv2020({ strict: false, allErrors: true }) addFormats(ajv) const validate = ajv.compile(schema) const valid = validate(config) expect(validate.errors ?? [], JSON.stringify(validate.errors, null, 2)).toEqual([]) expect(valid).toBe(true) }) it("is schema version 2 and names the compose file", () => { expect(config.schema_version).toBe(2) expect(config.compose.file).toBe("compose.yaml") }) it("declares web, worker and the migrate service, all of one image and none of them a job", () => { expect(Object.keys(config.services).sort()).toEqual(["migrate", "web", "worker"]) // kind "job" and pre_deploy_command are gone (KI-197): the migrate service is an ordinary // service whose ordering comes from compose's depends_on (AT-D14). for (const [name, service] of Object.entries(config.services)) { expect(service.kind, `${name}.kind`).toBeUndefined() expect(service.pre_deploy_command, `${name}.pre_deploy_command`).toBeUndefined() } }) it.runIf(!moduleSelected("guides"))("does not publish Guides before explicit deployment configuration", () => { expect(config.guides).toBeUndefined() for (const service of Object.values(config.services)) expect(service).not.toHaveProperty("guides") }) it.runIf(moduleSelected("guides"))("declares the selected starter’s native code-guides bundle", () => { expect(config.guides?.bundle).toBe("/app/.conusai/guides-bundle.json") }) it("exposes web alone, on its own health path", () => { expect(config.services.web?.public).toEqual({ port: 3000, primary: true }) // The path the platform probes is the path compose probes: one definition of "this web container is // fit to serve", /api/ready (store, reader, migration level), and not a second, weaker one. expect(config.services.web?.health_check?.path).toBe("/api/ready") const compose = readFileSync(new URL("../compose.yaml", import.meta.url), "utf8") expect(compose).toContain("http://127.0.0.1:3000/api/ready") expect(config.services.worker?.public).toBeUndefined() expect(config.services.migrate?.public).toBeUndefined() }) it("declares exactly one migrator, which is the migrate job (data-services DS21)", () => { const migrators = Object.entries(config.services).filter(([, service]) => service.postgres?.migrate === true) expect(migrators.map(([name]) => name)).toEqual(["migrate"]) }) it("links a Postgres service, and one sign-in link", () => { // The identity link is what injects OIDC_ISSUER/OIDC_WEB_CLIENT_ID/OIDC_AUDIENCE at // deploy (MIB I3); without the declaration the app boots signed-out-only (KI-199). const links = config.links as Array<{ type: string; role?: string }> expect(links[0]?.type).toBe("postgres") const signInLinks = ["identity"] signInLinks.push("better-auth") // kit:convex-better-auth -- the named Convex link replaces Identity (BL) expect(links.filter((link) => signInLinks.includes(link.role ?? link.type))).toHaveLength(1) }) it("keeps the actual base file, metering, account and branding scope minimums across profiles", () => { const base = ["blob:read", "blob:write", "usage:write", "billing:ingest", "privacy:subjects", "branding:read"] expect(config.token?.permissions).toEqual(expect.arrayContaining(base)) const profiles = new URL("../profiles/", import.meta.url) for (const profile of existsSync(profiles) ? readdirSync(profiles) : []) { const file = new URL(`${profile}/conusai.json`, profiles) if (!existsSync(file)) continue const copy = JSON.parse(readFileSync(file, "utf8")) expect(copy.token.permissions).toEqual(expect.arrayContaining(base)) if (!moduleSelected("guides")) expect(copy.guides).toBeUndefined() } }) })