340 lines
14 KiB
TypeScript
340 lines
14 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// The platform's services are really bound (plan item AT4.1, spec §9): the AI gateway, OCR, Blob, the
|
|
// audit log and usage metering, built from the deployment contract by `platformClients`, bound to the
|
|
// tokens modules resolve them by in BOTH composition roots (apps/web/src/server/ports.ts and
|
|
// apps/worker/src/ports.ts), and each one reaching the platform over HTTP. The platform here is the
|
|
// fake of @conusai-kit/testing; nothing between a token and its HTTP request is stubbed.
|
|
|
|
import { readFileSync } from "node:fs"
|
|
import { loadAppEnv, PlatformEnvError, platformClients, probePlatform, publicUrlOf } from "@app/config"
|
|
import { appModules } from "@app/modules"
|
|
import { DOCUMENT_MODEL, DOCUMENT_OCR, MODEL_GATEWAY } from "@app/modules/documents"
|
|
import { buildRegistry, Container } from "@conusai-kit/core"
|
|
import { OBJECT_STORE } from "@conusai-kit/data"
|
|
import { FILES_AUDIT, FILES_REGISTRY, FILES_USAGE } from "@conusai-kit/files"
|
|
import { PROPOSALS_AUDIT, PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
|
|
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
import { z } from "zod"
|
|
import { bindWebPorts } from "../apps/web/src/server/ports.js"
|
|
import { bindModulePorts } from "../apps/worker/src/ports.js"
|
|
|
|
const fixture = JSON.parse(
|
|
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
|
|
) as Record<string, unknown>
|
|
|
|
type Platform = Awaited<ReturnType<typeof startFakePlatform>>
|
|
|
|
let platform: Platform
|
|
beforeEach(async () => {
|
|
platform = await startFakePlatform()
|
|
})
|
|
afterEach(async () => {
|
|
await platform.close()
|
|
})
|
|
|
|
const environment = (extra: Record<string, string> = {}): Record<string, string | undefined> => ({
|
|
NODE_ENV: "test",
|
|
CONUSAI_FAKE_PLATFORM: "1",
|
|
CONUSAI_API_URL: `${platform.url}/api`,
|
|
CONUSAI_API_TOKEN: "test-token",
|
|
CONUSAI_PROJECT_ID: "42",
|
|
CONUSAI_ORG_ID: "42",
|
|
CONUSAI_ORG_SLUG: "owner-org",
|
|
POSTGRES_URL: "postgresql://app_app:secret@db:5432/app",
|
|
POSTGRES_READER_URL: "postgresql://app_reader:secret@db:5432/app",
|
|
...extra,
|
|
})
|
|
|
|
const setup = (service: "web" | "worker") => {
|
|
const env = loadAppEnv(service, environment())
|
|
const registry = buildRegistry([...appModules])
|
|
const clients = platformClients(env, registry, environment())
|
|
return { env, registry, clients }
|
|
}
|
|
|
|
/**
|
|
* What the fake platform recorded for one organization of the platform. It keeps every tenant apart,
|
|
* as the real one does: usage arrives under the organization in its URL, everything else under the
|
|
* default one.
|
|
*/
|
|
const stateOf = async (organization?: string) =>
|
|
(await (
|
|
await fetch(
|
|
`${platform.url}/__e2e/state`,
|
|
organization ? { headers: { "x-conusai-organization": organization } } : {},
|
|
)
|
|
).json()) as {
|
|
audit: Array<{ action: string; resource_id: string | null }>
|
|
usage: Array<Record<string, unknown>>
|
|
blobs: Array<{ pathname: string; size: number }>
|
|
ocr: unknown[]
|
|
ai: Array<{ request?: { model?: string } }>
|
|
}
|
|
|
|
describe("platformClients", () => {
|
|
it("writes an audit event to the platform's audit log", async () => {
|
|
const { clients } = setup("web")
|
|
await clients.audit.log("tenancy.organization_created", {
|
|
actor: { id: "user-1", kind: "human" },
|
|
target: { id: "acme", type: "organization" },
|
|
outcome: "success",
|
|
})
|
|
await clients.audit.flush()
|
|
expect((await stateOf()).audit.map((event) => event.action)).toContain("tenancy.organization_created")
|
|
})
|
|
|
|
it("meters usage against the ConusAI organization that owns the deployment, not against a customer", async () => {
|
|
const { clients } = setup("web")
|
|
clients.usage.track({
|
|
customer: { externalId: "acme" },
|
|
meter: "notes_created",
|
|
quantity: 1,
|
|
idempotencyKey: "n-1",
|
|
})
|
|
await clients.usage.flush()
|
|
// The fake separates numeric ID from slug, matching the production billing route contract.
|
|
const [fact] = (await stateOf("owner-org")).usage
|
|
expect((await stateOf()).usage).toEqual([])
|
|
expect(fact).toMatchObject({ meter: "notes_created", quantity: "1", idempotency_key: "n-1" })
|
|
})
|
|
|
|
it("stores an object in Blob and reads its bytes back", async () => {
|
|
const { clients } = setup("worker")
|
|
const stored = await clients.objectStore.put("files/a.txt", "hello", { contentType: "text/plain" })
|
|
expect((await stateOf()).blobs.map((blob) => blob.pathname)).toContain("files/a.txt")
|
|
expect(await (await clients.objectStore.download(stored.url)).text()).toBe("hello")
|
|
})
|
|
|
|
it("asks the AI gateway for a validated object, naming the model the app is configured with", async () => {
|
|
platform.ai.respond(() => ({ content: JSON.stringify({ answer: "four" }) }))
|
|
const { clients } = setup("worker")
|
|
const reply = await clients.models.generateObject(z.object({ answer: z.string() }), [
|
|
{ role: "user", content: "2 + 2?" },
|
|
])
|
|
expect(reply).toEqual({ answer: "four" })
|
|
const [call] = (await stateOf()).ai
|
|
expect(call?.request?.model).toBe(clients.model)
|
|
})
|
|
|
|
it("reads a document through the OCR service", async () => {
|
|
platform.ocr.setDocument(fixture)
|
|
const { clients } = setup("worker")
|
|
const job = await clients.documents.submit(new TextEncoder().encode("%PDF-1.4"), {
|
|
filename: "a.pdf",
|
|
idempotencyKey: "k-1",
|
|
})
|
|
const read = await clients.documents.wait(job, { intervalMs: 10 })
|
|
expect(read.pages).toHaveLength((fixture as { pages: unknown[] }).pages.length)
|
|
expect((await stateOf()).ocr).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe("what each process binds", () => {
|
|
const resolvable = (container: Container, tokens: Array<Parameters<Container["resolve"]>[0]>): boolean[] =>
|
|
tokens.map((token) => {
|
|
try {
|
|
container.resolve(token)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
|
|
it("binds Blob, the AI gateway, OCR and the registries in the web process", () => {
|
|
const { registry, clients } = setup("web")
|
|
const container = new Container()
|
|
bindWebPorts(container, clients, registry)
|
|
expect(
|
|
resolvable(container, [
|
|
OBJECT_STORE,
|
|
MODEL_GATEWAY,
|
|
DOCUMENT_MODEL,
|
|
DOCUMENT_OCR,
|
|
FILES_REGISTRY,
|
|
PROPOSALS_REGISTRY,
|
|
]),
|
|
).toEqual([true, true, true, true, true, true])
|
|
// The sinks that belong to the outbox are the worker's alone.
|
|
expect(resolvable(container, [FILES_AUDIT, FILES_USAGE, PROPOSALS_AUDIT])).toEqual([false, false, false])
|
|
})
|
|
|
|
it("binds the same five and the audit and usage sinks in the worker process", () => {
|
|
const { registry, clients } = setup("worker")
|
|
const container = new Container()
|
|
bindModulePorts(container, clients, registry)
|
|
expect(
|
|
resolvable(container, [
|
|
OBJECT_STORE,
|
|
MODEL_GATEWAY,
|
|
DOCUMENT_MODEL,
|
|
DOCUMENT_OCR,
|
|
FILES_REGISTRY,
|
|
PROPOSALS_REGISTRY,
|
|
FILES_AUDIT,
|
|
FILES_USAGE,
|
|
PROPOSALS_AUDIT,
|
|
]),
|
|
).toEqual([true, true, true, true, true, true, true, true, true])
|
|
})
|
|
|
|
it("sends the worker's usage and audit sinks to the platform, through the bound tokens", async () => {
|
|
const { registry, clients } = setup("worker")
|
|
const container = new Container()
|
|
bindModulePorts(container, clients, registry)
|
|
const usage = container.resolve(FILES_USAGE)
|
|
await usage.track({
|
|
customer: { externalId: "acme" },
|
|
meter: "files_bytes_stored",
|
|
quantity: 12,
|
|
idempotencyKey: "f-1",
|
|
})
|
|
await usage.flush()
|
|
await container.resolve(FILES_AUDIT).log("files.file_uploaded", {
|
|
actor: { id: "user-1", kind: "human" },
|
|
target: { id: "acme/file-1", type: "file" },
|
|
})
|
|
await clients.audit.flush()
|
|
const state = await stateOf()
|
|
expect((await stateOf("owner-org")).usage).toEqual([
|
|
expect.objectContaining({ meter: "files_bytes_stored", quantity: "12" }),
|
|
])
|
|
expect(state.audit.map((event) => event.action)).toContain("files.file_uploaded")
|
|
})
|
|
})
|
|
|
|
describe("the platform contract at start-up", () => {
|
|
const fake = (): Record<string, string | undefined> => environment()
|
|
|
|
it("refuses to start in production without the platform URL, naming the variable", () => {
|
|
const { CONUSAI_API_URL: _url, CONUSAI_FAKE_PLATFORM: _fake, ...rest } = fake()
|
|
const error = (() => {
|
|
try {
|
|
loadAppEnv("web", { ...rest, NODE_ENV: "production" })
|
|
} catch (thrown) {
|
|
return thrown
|
|
}
|
|
})()
|
|
expect(error).toBeInstanceOf(PlatformEnvError)
|
|
expect(error).toMatchObject({ variable: "CONUSAI_API_URL" })
|
|
expect((error as Error).message).toMatch(/CONUSAI_API_URL is not set: link this project to the platform/)
|
|
})
|
|
|
|
it("refuses to start without the platform token, whichever mode, naming the variable", () => {
|
|
for (const NODE_ENV of ["production", "development", undefined]) {
|
|
const { CONUSAI_API_TOKEN: _token, ...rest } = fake()
|
|
const run = () =>
|
|
loadAppEnv("worker", { ...rest, ...(NODE_ENV ? { NODE_ENV } : {}), CONUSAI_FAKE_PLATFORM: undefined })
|
|
expect(run, String(NODE_ENV)).toThrow(PlatformEnvError)
|
|
expect(run, String(NODE_ENV)).toThrow(/CONUSAI_API_TOKEN is not set/)
|
|
}
|
|
})
|
|
|
|
it("treats a run with no NODE_ENV as production, so the fake platform must be asked for in a development run", () => {
|
|
const { NODE_ENV: _mode, ...rest } = fake()
|
|
expect(() => loadAppEnv("web", rest)).toThrow(/CONUSAI_FAKE_PLATFORM is set in a production run/)
|
|
})
|
|
|
|
it("points a development run that has no platform at `pnpm dev` and the explicit switch", () => {
|
|
const { CONUSAI_API_URL: _url, CONUSAI_FAKE_PLATFORM: _fake, ...rest } = fake()
|
|
expect(() => loadAppEnv("web", { ...rest, NODE_ENV: "development" })).toThrow(/pnpm dev.*CONUSAI_FAKE_PLATFORM=1/)
|
|
})
|
|
|
|
it("accepts the fake platform in development and in a test, and says which it is using", () => {
|
|
for (const NODE_ENV of ["development", "test"]) {
|
|
expect(loadAppEnv("web", { ...fake(), NODE_ENV })).toMatchObject({ platform: "fake", production: false })
|
|
}
|
|
const { CONUSAI_FAKE_PLATFORM: _fake, ...real } = fake()
|
|
expect(loadAppEnv("web", { ...real, NODE_ENV: "development" }).platform).toBe("platform")
|
|
})
|
|
|
|
it("refuses the fake platform in a production run even when everything else is set", () => {
|
|
expect(() => loadAppEnv("web", { ...fake(), NODE_ENV: "production" })).toThrow(PlatformEnvError)
|
|
expect(() => loadAppEnv("web", { ...fake(), NODE_ENV: "production" })).toThrow(/CONUSAI_FAKE_PLATFORM/)
|
|
})
|
|
|
|
it("refuses the fake switch when the platform URL is somebody's public address", () => {
|
|
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "https://api.conusai.example/api" })).toThrow(
|
|
/CONUSAI_API_URL names api\.conusai\.example/,
|
|
)
|
|
// ... while a compose service name and loopback are what the fake platform runs at.
|
|
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "http://platform:4010/api" })).not.toThrow()
|
|
expect(() => loadAppEnv("web", { ...fake(), CONUSAI_API_URL: "http://127.0.0.1:4010/api" })).not.toThrow()
|
|
})
|
|
|
|
it("requires the owning organization in production, where usage would otherwise be metered against nothing", () => {
|
|
const { CONUSAI_FAKE_PLATFORM: _fake, CONUSAI_ORG_ID: _org, ...rest } = fake()
|
|
const production = { ...rest, NODE_ENV: "production" }
|
|
expect(() => loadAppEnv("web", production)).toThrow(/CONUSAI_ORG_ID is not set/)
|
|
expect(() => loadAppEnv("worker", production)).toThrow(/CONUSAI_ORG_ID is not set/)
|
|
// The migrate job meters nothing.
|
|
expect(() =>
|
|
loadAppEnv("migrate", { ...production, POSTGRES_MIGRATE_URL: "postgresql://app_owner:secret@db:5432/app" }),
|
|
).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe("probePlatform", () => {
|
|
it("uses public readiness when the platform descriptor is protected", async () => {
|
|
const authorization: Array<string | null> = []
|
|
const answered = await probePlatform(
|
|
{ CONUSAI_API_URL: "https://platform.example.test/api" },
|
|
{
|
|
fetch: async (input, init) => {
|
|
authorization.push(new Headers(init?.headers).get("authorization"))
|
|
const path = new URL(String(input)).pathname
|
|
// The real platform mounts its descriptor under authenticated /api;
|
|
// /readyz is its public readiness contract.
|
|
if (path === "/readyz") return new Response("ready")
|
|
return new Response("not public", { status: path.startsWith("/api/") ? 401 : 404 })
|
|
},
|
|
},
|
|
)
|
|
expect(answered).toBe(true)
|
|
expect(authorization).toEqual([null])
|
|
})
|
|
|
|
it("is true when the platform answers its public readiness probe, and sends no credential", async () => {
|
|
const seen: Array<string | null> = []
|
|
const answered = await probePlatform(
|
|
{ CONUSAI_API_URL: `${platform.url}/api` },
|
|
{
|
|
fetch: async (input, init) => {
|
|
seen.push(new Headers(init?.headers).get("authorization"))
|
|
return fetch(input, init)
|
|
},
|
|
},
|
|
)
|
|
expect(answered).toBe(true)
|
|
expect(seen).toEqual([null])
|
|
})
|
|
|
|
it("is false when nothing answers, without throwing", async () => {
|
|
expect(await probePlatform({ CONUSAI_API_URL: "http://127.0.0.1:9/api" }, { timeoutMs: 500 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("the public address", () => {
|
|
it("is the one the deployment states", () => {
|
|
expect(publicUrlOf({ NODE_ENV: "production", CONUSAI_PUBLIC_URL: "https://app.example.test" })).toBe(
|
|
"https://app.example.test",
|
|
)
|
|
})
|
|
|
|
it("is refused by name in a production run that has none, and never guessed", () => {
|
|
for (const env of [{ NODE_ENV: "production" }, {}]) {
|
|
expect(() => publicUrlOf(env)).toThrow(PlatformEnvError)
|
|
try {
|
|
publicUrlOf(env)
|
|
} catch (error) {
|
|
expect((error as PlatformEnvError).variable).toBe("CONUSAI_PUBLIC_URL")
|
|
}
|
|
}
|
|
})
|
|
|
|
it("falls back to the development address only in development and tests", () => {
|
|
expect(publicUrlOf({ NODE_ENV: "development" })).toBe("http://localhost:3000")
|
|
expect(publicUrlOf({ NODE_ENV: "test" })).toBe("http://localhost:3000")
|
|
})
|
|
})
|