111 lines
5.2 KiB
TypeScript
111 lines
5.2 KiB
TypeScript
// 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()
|
||
}
|
||
})
|
||
})
|