Files
template-app/test/conusai-json.test.ts
2026-11-14 09:56:45 +00:00

111 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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()
}
})
})