133 lines
6.7 KiB
TypeScript
133 lines
6.7 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 { 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"
|
|
|
|
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()
|
|
}
|
|
})
|
|
|
|
// kit:guides:begin
|
|
it("declares the code-guides bundle the image build writes, at an absolute path (ADR-045)", () => {
|
|
expect(config.guides).toEqual({ bundle: "/app/.conusai/guides-bundle.json" })
|
|
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
|
|
// The build stage's WORKDIR is /app and the runtime stage copies /app whole, so the file the
|
|
// build writes is where conusai.json says the platform will find it.
|
|
expect(dockerfile).toContain(`node apps/worker/dist/guides-bundle.js ${config.guides?.bundle} guides-anchors.json`)
|
|
})
|
|
|
|
// kit:guides:end
|
|
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("requests the platform's canonical runtime scopes for document processing and usage metering", () => {
|
|
const permissions = config.token?.permissions ?? []
|
|
expect(permissions).toEqual(expect.arrayContaining(["ocr:use", "ai:execute", "usage:write"]))
|
|
expect(permissions).not.toContain("billing:ingest")
|
|
|
|
const maria = JSON.parse(readFileSync(new URL("../profiles/mariadb/conusai.json", import.meta.url), "utf8")) as {
|
|
token?: { permissions: string[] }
|
|
}
|
|
expect(maria.token?.permissions).toEqual(expect.arrayContaining(["ocr:use", "ai:execute", "usage:write"]))
|
|
expect(maria.token?.permissions).not.toContain("billing:ingest")
|
|
})
|
|
|
|
// The account page's platform analytics erasure and export (ADR-049) call the deployment-token
|
|
// routes, which take `privacy:subjects` by name: `*` never carries it, so it is listed explicitly.
|
|
it("requests privacy:subjects for the account page's analytics erasure and export", () => {
|
|
expect(config.token?.permissions).toContain("privacy:subjects")
|
|
expect(config.token?.permissions).not.toContain("*")
|
|
})
|
|
|
|
// Every selectable profile's conusai.json replaces this one at the app root
|
|
// (`kit template:prune --profile`), so each must request what the enabled modules need: the
|
|
// Convex profile asked for none, so a Convex app's deployment token lacked guides:runtime and
|
|
// support:widget-sessions and its guides and support calls were refused. `template:prune --without`
|
|
// removes a module's permission from every copy, so the lists stay equal after a prune too.
|
|
it.each(["convex", "mariadb"])("the %s profile requests the same runtime scopes as the default", (profile) => {
|
|
const copy = JSON.parse(readFileSync(new URL(`../profiles/${profile}/conusai.json`, import.meta.url), "utf8")) as {
|
|
token?: { permissions: string[] }
|
|
}
|
|
const moduleScopes: string[] = []
|
|
moduleScopes.push("guides:runtime") // kit:guides
|
|
moduleScopes.push("support:widget-sessions") // kit:support
|
|
expect(config.token?.permissions).toEqual(expect.arrayContaining(moduleScopes))
|
|
expect([...(copy.token?.permissions ?? [])].sort()).toEqual([...(config.token?.permissions ?? [])].sort())
|
|
})
|
|
})
|