ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The coding-agent contract this template ships: CLAUDE.md imports AGENTS.md instead of repeating it, the
// committed Claude Code settings keep secrets unreadable and a push behind a person, the support-report skill is
// readable by its tool, and the format-on-edit hook formats only this repository's own source files.
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { fileToFormat } from "../scripts/agent-format.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const read = (path: string) => readFileSync(join(APP_ROOT, path), "utf8")
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
describe("the coding-agent contract", () => {
it("has CLAUDE.md import AGENTS.md, the one source of the rules", () => {
expect(read("CLAUDE.md")).toMatch(/^@AGENTS\.md$/m)
expect(read("AGENTS.md")).toContain("## Work that starts from a support report")
})
it("commits Claude Code settings that refuse .env reads and ask before a push or a migration run", () => {
const settings = JSON.parse(read(".claude/settings.json"))
expect(settings.permissions.deny).toEqual(expect.arrayContaining(["Read(/.env)", "Read(/.env.local)"]))
expect(settings.permissions.ask).toEqual(expect.arrayContaining(["Bash(git push *)", "Bash(pnpm migrate *)"]))
expect(settings.permissions.allow).toContain("Bash(pnpm gate)")
// Nothing that publishes or deploys is pre-approved.
for (const rule of settings.permissions.allow) expect(rule).not.toMatch(/push|migrate|deploy/)
expect(read(".gitignore")).toContain(".claude/settings.local.json")
})
it("ships the support-report skill with the frontmatter its tool reads first", () => {
const skill = read(".claude/skills/fix-support-report/SKILL.md")
expect(skill.startsWith("---\nname: fix-support-report\ndescription: ")).toBe(true)
expect(skill).toMatch(/Reproduce it before changing code/)
})
})
describe("the format-on-edit hook", () => {
it("names a repository source file Biome formats, and nothing else", () => {
const root = mkdtempSync(join(tmpdir(), "agent-format-"))
made.push(root)
writeFileSync(join(root, "a.ts"), "export const a = 1\n")
writeFileSync(join(root, "notes.md"), "# notes\n")
expect(fileToFormat({ tool_input: { file_path: join(root, "a.ts") } }, root)).toBe("a.ts")
expect(fileToFormat({ tool_input: { file_path: "a.ts" } }, root)).toBe("a.ts")
expect(fileToFormat({ tool_input: { file_path: "notes.md" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: { file_path: "missing.ts" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: { file_path: "../outside.ts" } }, root)).toBeNull()
expect(fileToFormat({ tool_input: {} }, root)).toBeNull()
expect(fileToFormat(null, root)).toBeNull()
})
})
+141
View File
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `agent.run.events` through the kernel's caller, the way the HTTP mount calls it: a project's
// reader gets the run's AG-UI events as Server-Sent Events, resumes strictly after an offset, and
// the stream closes after the run's terminal event; another organization's run is not found, as
// `agent.run.status` answers, and an offset of another run is refused.
import { AppError, type TransportCaller } from "@conusai-kit/core"
import { describe, expect, it, vi } from "vitest"
import { makeNotesApp, type NotesApp, type SeededProject } from "./support/notes-app.js"
const PLAN = [{ key: "read", label: "Read", tool: "documents.read", args: { fileId: "f-1" } }]
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
interface Frame {
id: string | undefined
data: { type: string; [field: string]: unknown }
}
const framesOf = (text: string): Frame[] =>
text
.split("\n\n")
.filter((frame) => frame.trim() !== "" && !frame.startsWith(":"))
.map((frame) => ({
id: /^id: (.*)$/m.exec(frame)?.[1],
data: JSON.parse(/^data: (.*)$/m.exec(frame)?.[1] ?? "null") as Frame["data"],
}))
/** The first chunk of a stream that is still open (a run that has not ended), then the stream is let go. */
async function firstChunk(response: Response): Promise<string> {
const reader = (response.body as ReadableStream<Uint8Array>).getReader()
const { value } = await reader.read()
await reader.cancel()
return new TextDecoder().decode(value)
}
async function startRun(app: NotesApp, project: SeededProject) {
const started = (await project.call.callByName("agent.run.start", {
...project.slugs,
goal: "Review",
plan: PLAN,
})) as {
runId: string
}
// The resume event names the run's project by id: what the engine would write under.
const record = (await app.data.outbox.pending(50)).find(
(entry) =>
entry.name === "agent.runResumeRequested" && (entry.payload as { runId: string }).runId === started.runId,
)
const ids = record?.payload as { orgId: string; workspaceId: string; projectId: string }
const scope = { level: "project" as const, actor: project.owner, ...ids }
/** Ends the run as the engine would. */
const finish = async () => {
const entity = app.entities.find((candidate) => candidate.table === "agent_run")
if (entity?.tenancy !== "project") throw new Error("the agent module declares no project run table")
const scoped = app.data.forScope(scope)
const table = scoped.of(entity as Parameters<typeof scoped.of>[0])
const row = (await table.get(started.runId)) as { version: number } | null
if (!row) throw new Error("the run is gone")
await table.update(started.runId, { status: "cancelled" }, row.version)
}
return { runId: started.runId, finish }
}
const events = (call: TransportCaller, input: Record<string, unknown>) =>
call.callByName("agent.run.events", input) as Promise<Response>
describe("agent.run.events", () => {
it("streams a reader the run's events, resumes after an offset and closes after the terminal event", async () => {
const app = makeNotesApp()
const project = await app.seedProject("acme", "owner@acme.test")
const run = await startRun(app, project)
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const open = await events(viewer.call, { ...project.slugs, runId: run.runId })
expect(open.headers.get("content-type")).toContain("text/event-stream")
expect(open.headers.get("cache-control")).toBe("no-store")
expect(framesOf(await firstChunk(open))).toEqual([
{
id: `agk1:${run.runId}:0`,
data: { type: "RUN_STARTED", threadId: `agent-run:${run.runId}`, runId: run.runId },
},
])
await run.finish()
const rest = framesOf(
await (await events(viewer.call, { ...project.slugs, runId: run.runId, after: `agk1:${run.runId}:0` })).text(),
)
expect(rest).toEqual([
{
id: `agk1:${run.runId}:1`,
data: {
type: "RUN_FINISHED",
threadId: `agent-run:${run.runId}`,
runId: run.runId,
outcome: { type: "success" },
result: { status: "cancelled" },
},
},
])
expect(await (await events(viewer.call, { ...project.slugs, runId: run.runId, after: "1" })).text()).toBe("")
})
it("answers another organization's run as not found, and refuses another run's offset", async () => {
const app = makeNotesApp()
const acme = await app.seedProject("acme", "owner@acme.test")
const globex = await app.seedProject("globex", "owner@globex.test")
const theirs = await startRun(app, acme)
const ours = await startRun(app, globex)
// Globex's member names Acme's run in Globex's own project: not found, as agent.run.status says.
const asStatus = await refusal(globex.call.callByName("agent.run.status", { ...globex.slugs, runId: theirs.runId }))
const asEvents = await refusal(events(globex.call, { ...globex.slugs, runId: theirs.runId }))
expect(asEvents.status).toBe(404)
expect(asEvents.code).toBe(asStatus.code)
// ... and in Acme's project, of which they are no member: refused before anything is read.
const outside = await refusal(events(globex.call, { ...acme.slugs, runId: theirs.runId }))
expect([403, 404]).toContain(outside.status)
const foreignOffset = await refusal(
events(globex.call, { ...globex.slugs, runId: ours.runId, after: `agk1:${theirs.runId}:0` }),
)
expect(foreignOffset.status).toBe(422)
const malformed = await refusal(events(globex.call, { ...globex.slugs, runId: ours.runId, after: "../0" }))
expect(malformed.status).toBe(422)
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["agent"]),
}))
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The one agent setting of the app (G3j): how many days a finished run is kept before the hourly sweep
// deletes it. A whole number of days from 1 to 3650, 90 by default; anything else is refused with the name
// of the variable, never its value.
import { agentRunRetentionDaysOf } from "@app/config"
import { describe, expect, it } from "vitest"
describe("AGENT_RUN_RETENTION_DAYS", () => {
it("is 90 days when unset or blank, and the value it is set to otherwise", () => {
expect(agentRunRetentionDaysOf({})).toBe(90)
expect(agentRunRetentionDaysOf({ AGENT_RUN_RETENTION_DAYS: " " })).toBe(90)
expect(agentRunRetentionDaysOf({ AGENT_RUN_RETENTION_DAYS: " 30 " })).toBe(30)
expect(agentRunRetentionDaysOf({ AGENT_RUN_RETENTION_DAYS: "1" })).toBe(1)
expect(agentRunRetentionDaysOf({ AGENT_RUN_RETENTION_DAYS: "3650" })).toBe(3650)
})
it.each(["0", "3651", "1.5", "-3", "ninety"])(
"refuses %s with a message that names the variable and not the value",
(value) => {
// The message is fixed text: it cannot echo what was set, so it cannot leak a value into a log.
expect(() => agentRunRetentionDaysOf({ AGENT_RUN_RETENTION_DAYS: value })).toThrow(
new Error("AGENT_RUN_RETENTION_DAYS must be a whole number of days from 1 to 3650"),
)
},
)
})
+162
View File
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The deployment contract per process (decision AT-D20): which Postgres role each process may
// hold, and what happens when it is handed one it may not.
import { describe, expect, it } from "vitest"
import { loadAppEnv, roleOf } from "../config/src/env.js"
import { assertBoundByRowLevelSecurity, assertOwnsSchema } from "../config/src/postgres.js"
const base = {
CONUSAI_API_URL: "https://api.example.test/api",
CONUSAI_API_TOKEN: "token",
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "9",
CONUSAI_ORG_SLUG: "acme",
POSTGRES_URL: "postgresql://app_app:secret@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:secret@db:5432/app",
}
describe("the deployment contract", () => {
it("gives web and worker the runtime role and the global reader", () => {
for (const service of ["web", "worker"] as const) {
const env = loadAppEnv(service, { ...base })
expect(env.postgres?.runtime).toContain("app_app")
expect(env.postgres?.reader).toContain("app_reader")
expect(env.postgres?.owner).toBeUndefined()
}
})
it("refuses to start web or worker with the owner's URL", () => {
for (const service of ["web", "worker"] as const) {
expect(() =>
loadAppEnv(service, { ...base, POSTGRES_MIGRATE_URL: "postgresql://app_owner:s@db:5432/app" }),
).toThrow(/POSTGRES_MIGRATE_URL is set for the \w+ process/)
}
})
it("requires the platform organization slug for production web and worker usage billing, but not migrate", () => {
const { CONUSAI_ORG_SLUG: _slug, ...withoutSlug } = base
for (const service of ["web", "worker"] as const) {
expect(() => loadAppEnv(service, { ...withoutSlug })).toThrow(/CONUSAI_ORG_SLUG/)
}
expect(() =>
loadAppEnv("migrate", {
...withoutSlug,
POSTGRES_MIGRATE_URL: "postgresql://app_owner:secret@db:5432/app",
}),
).not.toThrow()
})
it("refuses the migrate job without the owner's URL, naming postgres.migrate", () => {
expect(() => loadAppEnv("migrate", { ...base })).toThrow(/declares postgres\.migrate/)
})
it("refuses the migrate job when two of the three URLs name one role", () => {
expect(() => loadAppEnv("migrate", { ...base, POSTGRES_MIGRATE_URL: base.POSTGRES_URL })).toThrow(
/three separate logins/,
)
})
it("names the Postgres service to link when a URL is missing", () => {
const { POSTGRES_URL: _dropped, ...without } = base
expect(() => loadAppEnv("web", without)).toThrow(/link a Postgres service/)
})
it("refuses a runtime URL and a reader URL that name one login", () => {
expect(() => loadAppEnv("web", { ...base, POSTGRES_READER_URL: base.POSTGRES_URL })).toThrow(/name the same role/)
})
it("refuses a role name that is not a plain identifier", () => {
expect(roleOf("postgresql://app_reader:s@db:5432/app")).toBe("app_reader")
expect(() => roleOf("postgresql://app%2Dreader:s@db:5432/app")).toThrow(/not a plain identifier/)
})
it("loads Convex server credentials and refuses missing or misplaced credentials", () => {
const convex = {
...base,
CONVEX_SELF_HOSTED_URL: "http://convex:3210",
CONVEX_SELF_HOSTED_ADMIN_KEY: "server-only-test-key",
}
const loaded = loadAppEnv("web", convex, "convex")
expect(loaded.convex).toEqual({ url: "http://convex:3210", adminKey: "server-only-test-key" })
expect(loaded).not.toHaveProperty("postgres")
const { CONVEX_SELF_HOSTED_ADMIN_KEY: _key, ...missingKey } = convex
expect(() => loadAppEnv("web", missingKey, "convex")).toThrow(/CONVEX_SELF_HOSTED_ADMIN_KEY/)
expect(() => loadAppEnv("migrate", convex, "convex")).toThrow(/not supported by the Convex app runtime/)
})
it("gives every process of the mariadb profile the one MARIADB_URL, and web and worker the queue's REDIS_URL", () => {
const { POSTGRES_URL: _p, POSTGRES_READER_URL: _r, ...platform } = base
const mariadb = {
...platform,
MARIADB_URL: "mysql://app:secret@mariadb:3306/app",
DATABASE_URL: "mysql://app:secret@mariadb:3306/app",
REDIS_URL: "redis://valkey:6379",
}
for (const service of ["web", "worker"] as const) {
const loaded = loadAppEnv(service, mariadb, "mariadb")
expect(loaded.mariadb).toEqual({ url: mariadb.MARIADB_URL })
expect(loaded.redis).toEqual({ url: "redis://valkey:6379" })
expect(loaded).not.toHaveProperty("postgres")
const { REDIS_URL: _q, ...noQueue } = mariadb
expect(() => loadAppEnv(service, noQueue, "mariadb")).toThrow(/REDIS_URL is not set/)
}
const migrate = loadAppEnv("migrate", { ...mariadb, REDIS_URL: "" }, "mariadb")
expect(migrate.mariadb?.url).toBe(mariadb.MARIADB_URL)
expect(migrate).not.toHaveProperty("redis")
const { MARIADB_URL: _m, ...noStore } = mariadb
expect(() => loadAppEnv("web", noStore, "mariadb")).toThrow(/MARIADB_URL is not set: link a MariaDB service/)
})
it("gives every process of the mongodb profile the one MONGODB_URL and refuses one without it", () => {
const { POSTGRES_URL: _p, POSTGRES_READER_URL: _r, ...platform } = base
const mongodb = {
...platform,
MONGODB_URL: "mongodb://app:secret@mongodb:27017/app?directConnection=true",
REDIS_URL: "redis://valkey:6379",
}
const web = loadAppEnv("web", mongodb, "mongodb")
expect(web.mongodb).toEqual({ url: mongodb.MONGODB_URL })
expect(web).not.toHaveProperty("mariadb")
expect(loadAppEnv("migrate", mongodb, "mongodb")).not.toHaveProperty("redis")
const { MONGODB_URL: _m, ...noStore } = mongodb
expect(() => loadAppEnv("worker", noStore, "mongodb")).toThrow(/MONGODB_URL/)
})
})
describe("the role the database answers with", () => {
const facts = {
role: "app_app",
ownedTables: 0,
bypassesRowLevelSecurity: false,
isSuperuser: false,
ownsSchema: false,
}
it("accepts a role that owns nothing and bypasses nothing", () => {
expect(assertBoundByRowLevelSecurity(facts, "POSTGRES_URL")).toBe(facts)
})
it("refuses a connection that owns a table, bypasses row-level security or is a superuser", () => {
expect(() => assertBoundByRowLevelSecurity({ ...facts, ownedTables: 3 }, "POSTGRES_URL")).toThrow(/owns 3 table/)
expect(() => assertBoundByRowLevelSecurity({ ...facts, bypassesRowLevelSecurity: true }, "POSTGRES_URL")).toThrow(
/BYPASSRLS/,
)
expect(() => assertBoundByRowLevelSecurity({ ...facts, isSuperuser: true }, "POSTGRES_READER_URL")).toThrow(
/superuser/,
)
})
it("refuses a migrate job that does not own schema public", () => {
expect(() => assertOwnsSchema(facts, "POSTGRES_MIGRATE_URL")).toThrow(/does not own schema public/)
expect(assertOwnsSchema({ ...facts, role: "app_owner", ownsSchema: true }, "POSTGRES_MIGRATE_URL").ownsSchema).toBe(
true,
)
})
it("names the variable whose connection is wrong", () => {
expect(() => assertBoundByRowLevelSecurity({ ...facts, ownedTables: 1 }, "POSTGRES_READER_URL")).toThrow(
/POSTGRES_READER_URL/,
)
})
})
+43
View File
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { afterEach, expect, it, vi } from "vitest"
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
/** The build's real configuration, evaluated with this environment. */
const viteConfig = async () => {
vi.resetModules()
const { default: config } = await import("../apps/web/vite.config")
return config as {
base?: string
envPrefix?: unknown
define?: unknown
build?: { assetsDir?: string; outDir?: string }
}
}
it("keeps compiled chunk URLs stable when the platform injects a deployment asset prefix", async () => {
// Compose serves the image's assets itself. Vite writes its chunk URLs into the build, so a base read at
// startup would leave the browser waiting for chunks under a name nothing serves: one image, every
// environment, and no `base` in the build at all.
vi.stubEnv("CONUSAI_ASSET_PREFIX", "/_conusai/assets/app-42")
const config = await viteConfig()
expect(config.base ?? "").toBe("")
})
it("writes the build where server.mjs serves it from, under the folder RESERVED_SLUGS keeps from organizations", async () => {
const config = await viteConfig()
expect(config.build?.outDir).toBe("dist")
// Vite's default; test/reserved-routes.test.ts reserves `assets` because of it.
expect(config.build?.assetsDir ?? "assets").toBe("assets")
})
it("hands the browser's bundle no variable: Vite's `VITE_` prefix stays the only way in, and nothing uses it", async () => {
// A wider `envPrefix` or a `define` is how a server variable would reach a chunk (spec §6.4: the server
// writes what the browser needs into the page, and there is no build-time public value).
const config = await viteConfig()
expect(config.envPrefix).toBeUndefined()
expect(config.define).toBeUndefined()
})
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The words for each sign-in failure's code. Which code a failure gets, and where it may redirect to,
// are `@conusai-kit/host/postgres`'s, with their tests (kit-host `auth-errors.test.ts`).
import { describe, expect, it } from "vitest"
import { AUTH_ERROR_MESSAGES, AUTH_ERRORS } from "../apps/web/src/server/auth-errors.js"
import { appCatalogues } from "../apps/web/src/ui/messages.js"
describe("the words for each code", () => {
it("exist in English and in Lithuanian for every code", () => {
for (const code of AUTH_ERRORS) {
const [module, ...rest] = (AUTH_ERROR_MESSAGES[code] as string).split(".")
const key = rest.join(".")
expect(appCatalogues.en?.[module as string]?.[key], `en ${code}`).toBeTruthy()
expect(appCatalogues.lt?.[module as string]?.[key], `lt ${code}`).toBeTruthy()
}
})
})
+106
View File
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The page's unload path (apps/web/src/ui/client.ts `beacon`): a call made as the page goes away is
// a same-origin keep-alive POST to the procedure's mount with the page's CSRF token, and the
// server's real CSRF wall admits exactly that request for this session and refuses it for another.
// The browser half (keep-alive after a navigation and a closed tab) is proven in a real browser by
// @conusai-kit/guides' Playwright suite (test-browser/beacon.spec.ts).
import type { Principal } from "@conusai-kit/auth"
import { afterEach, beforeAll, describe, expect, it } from "vitest"
const ORIGIN = "https://app.example.test"
const session = (sessionId: string): Principal => ({
actor: { userId: "11111111-1111-1111-1111-111111111111" },
method: "session",
sessionId,
})
let csrfToken: (sessionId: string) => string
let requireCsrf: (request: Request, principal: Principal | null) => void
let beacon: (procedure: string, input: unknown) => boolean
beforeAll(async () => {
process.env.AUTH_SESSION_SECRET = "0123456789abcdef0123456789abcdef"
process.env.CONUSAI_PUBLIC_URL = ORIGIN
process.env.OIDC_ISSUER = "https://id.example.test"
process.env.OIDC_WEB_CLIENT_ID = "web"
process.env.OIDC_AUDIENCE = "api"
const module = await import("@conusai-kit/host/http")
csrfToken = module.csrfToken
requireCsrf = module.requireCsrf
// Imported before any page global is stubbed: the beacon reads the page at call time, and the UI
// kit's dependencies set up browser listeners on import when they see a window.
beacon = (await import("../apps/web/src/ui/client")).beacon
})
const globals = globalThis as unknown as { window?: unknown; document?: unknown; fetch: typeof fetch }
const realFetch = globals.fetch
afterEach(() => {
globals.fetch = realFetch
delete globals.window
delete globals.document
})
/** A page of this app holding `token` in its CSRF meta tag; returns what the page's fetch was handed. */
function page(token: string): Array<[URL, RequestInit]> {
const sent: Array<[URL, RequestInit]> = []
globals.window = { location: { href: `${ORIGIN}/acme/main/notes/notes`, origin: ORIGIN } }
globals.document = {
querySelector: (selector: string) =>
selector === 'meta[name="csrf-token"]'
? { getAttribute: (name: string) => (name === "content" ? token : null) }
: null,
}
globals.fetch = ((url: URL, init: RequestInit) => {
sent.push([url, init])
return Promise.resolve(new Response(null, { status: 200 }))
}) as typeof fetch
return sent
}
describe("the page's beacon", () => {
it("is a same-origin keep-alive POST to the procedure that the CSRF wall admits for this session only", () => {
const sent = page(csrfToken("session-1"))
const input = {
org: "acme",
idempotencyKey: "guides:1",
events: [{ schema_version: 1, event_id: "e1", type: "track" }],
}
expect(beacon("guides.runtime.record", input)).toBe(true)
expect(sent).toHaveLength(1)
const [url, init] = sent[0] as [URL, RequestInit]
expect(url.href).toBe(`${ORIGIN}/api/v1/guides.runtime.record`)
expect(init).toMatchObject({
method: "POST",
keepalive: true,
credentials: "same-origin",
body: JSON.stringify(input),
})
// What the server receives: the page's headers, plus the fetch metadata the browser adds to a
// request of this page's own origin.
const request = new Request(url, {
method: "POST",
headers: { ...(init.headers as Record<string, string>), "sec-fetch-site": "same-origin" },
body: init.body ?? null,
})
expect(() => requireCsrf(request, session("session-1"))).not.toThrow()
expect(() => requireCsrf(request, session("session-2"))).toThrow(/x-csrf-token/)
})
it("is declined when the body would not fit the keep-alive budget, so the engine keeps the batch", () => {
const sent = page(csrfToken("session-1"))
expect(beacon("guides.runtime.record", { padding: "x".repeat(70_000) })).toBe(false)
expect(sent).toHaveLength(0)
})
})
describe("the browser's half of the API path", () => {
it("types a call by its contract's own schemas", async () => {
// A compile-time property, checked here by construction: `callContract` takes the contract's
// input type and answers its output type. If either drifted, this file would not typecheck.
const { callContract } = await import("../apps/web/src/ui/client")
const contract = { name: "notes.note.create", input: { _input: { title: "" } }, output: { _output: { id: "" } } }
expect(typeof callContract).toBe("function")
expect(contract.name).toBe("notes.note.create")
})
})
+400
View File
@@ -0,0 +1,400 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The Convex Better Auth adapter of this app (AUTH_PROVIDER=convex-better-auth, joint contract §14),
// over fake transports: the deployment contract, the sign-out (derived service sessions first, then
// Better Auth, then every cookie), the account's end and the deletion's Better Auth step bound in the
// web and the worker, the relay's account events, the form's own password rule, and the locally
// installed component schema with the admin plugin's fields, which is
// generated from the app's Better Auth options. A real Better Auth session on the platform's pinned
// self-hosted Convex runtime, in a browser, is e2e/better-auth (gate J-auth); this file proves no
// live integration. `kit template:prune --auth oidc` removes it.
import { readFileSync } from "node:fs"
import { loadAppEnv, platformClients } from "@app/config"
import { appModules } from "@app/modules"
import { ACCOUNT_IDENTITY_PROVIDER } from "@app/modules/account"
import { buildRegistry, Container } from "@conusai-kit/core"
import { createMemoryDataSource, entitiesOf } from "@conusai-kit/data"
import { requireAppUser, tenancyModule } from "@conusai-kit/tenancy"
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
import { convexAdapter } from "@convex-dev/better-auth"
import { describe, expect, it, vi } from "vitest"
import { parse } from "yaml"
import { bindModulePorts } from "../apps/worker/src/ports.js"
import { betterAuthOptions, MIN_PASSWORD_LENGTH as OPTIONS_MIN } from "../better-auth/functions/betterAuth/options"
// The composition root is not under test here; better-auth.ts reaches it only to build its singleton.
vi.mock("../apps/web/src/server/app", () => ({
webRuntime: () => {
throw new Error("not in this test")
},
sessionRevoked: async () => undefined,
}))
const {
betterAuthEnv,
betterAuthFormLabels,
bindBetterAuthAccountEnd,
createBetterAuthRuntime,
deliverAccountEvent,
MIN_PASSWORD_LENGTH,
} = await import("../apps/web/src/server/better-auth")
const ENV = {
BETTER_AUTH_CONVEX_URL: "http://cloud.test",
BETTER_AUTH_CONVEX_SITE_URL: "http://site.test/",
BETTER_AUTH_CONVEX_ADMIN_KEY: "admin-key-of-the-test",
BETTER_AUTH_RELAY_INTERVAL_MS: "60000",
CONUSAI_PUBLIC_URL: "http://app.test",
}
const config = { provider: "convex-better-auth" as const, identityNamespace: "better-auth:test" }
describe("the Better Auth deployment contract", () => {
it("reads the deployment's URLs, its admin key and the relay interval", () => {
expect(betterAuthEnv(ENV)).toEqual({
url: "http://cloud.test",
siteUrl: "http://site.test",
adminKey: "admin-key-of-the-test",
relayIntervalMs: 60000,
})
expect(betterAuthEnv({ ...ENV, BETTER_AUTH_RELAY_INTERVAL_MS: "" }).relayIntervalMs).toBe(2000)
})
it("refuses a missing or malformed variable by name, never by value", () => {
for (const [name, value, message] of [
["BETTER_AUTH_CONVEX_URL", "", /BETTER_AUTH_CONVEX_URL is not set/],
["BETTER_AUTH_CONVEX_SITE_URL", "site.test", /BETTER_AUTH_CONVEX_SITE_URL is not an absolute/],
["BETTER_AUTH_CONVEX_ADMIN_KEY", " ", /BETTER_AUTH_CONVEX_ADMIN_KEY is not set/],
["BETTER_AUTH_RELAY_INTERVAL_MS", "10", /BETTER_AUTH_RELAY_INTERVAL_MS/],
] as const) {
expect(() => betterAuthEnv({ ...ENV, [name]: value }), name).toThrow(message)
}
expect(() => betterAuthEnv({ ...ENV, BETTER_AUTH_CONVEX_ADMIN_KEY: "" })).not.toThrow(/admin-key-of-the-test/)
})
})
type Seen = { url: string; init: RequestInit | undefined }
/** Better Auth's token endpoint, the validation query, the sign-out and the admin mutation, faked. */
function fakeDeployment() {
const seen: Seen[] = []
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
seen.push({ url, init })
if (url === "http://site.test/api/auth/convex/token") return Response.json({ token: "convex-token" })
if (url === "http://site.test/api/auth/sign-out") return Response.json({ success: true })
if (url.startsWith("http://cloud.test/api/query")) {
const body = JSON.parse(String(init?.body)) as { path: string }
if (body.path === "conusaiAuth:pendingRevocations")
return Response.json({ status: "success", value: [], logLines: [] })
const value = {
userId: "ba-user",
sessionId: "ba-session",
expiresAt: Date.now() + 3_600_000,
createdAt: Date.now() - 1000,
email: "ada@example.test",
name: "Ada",
emailVerified: false,
}
return Response.json({ status: "success", value, logLines: [] })
}
if (url.startsWith("http://cloud.test/api/mutation"))
return Response.json({ status: "success", value: null, logLines: [] })
throw new Error(`unexpected ${url}`)
}) as typeof globalThis.fetch
return { fetch, seen }
}
function runtimeWith(onSessionRevoked = vi.fn(async () => undefined)) {
const registry = buildRegistry([tenancyModule])
const identities = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const deployment = fakeDeployment()
const warnings: string[] = []
const runtime = createBetterAuthRuntime({
config,
identities,
onSessionRevoked,
onAccountEvent: async () => undefined,
log: { warn: (message) => warnings.push(message) },
env: ENV,
fetch: deployment.fetch,
})
return { runtime, deployment, onSessionRevoked, warnings }
}
const signedIn = (cookie = "better-auth.session_token=tok.sig") =>
new Request("http://app.test/auth/sign-out", { method: "POST", headers: { cookie } })
describe("the Better Auth sign-out", () => {
it("ends the device's derived service sessions, then the session at Better Auth, then every cookie", async () => {
const { runtime, deployment, onSessionRevoked } = runtimeWith()
try {
const response = await runtime.signOut(signedIn())
expect(onSessionRevoked).toHaveBeenCalledWith(
{ provider: "convex-better-auth", opaqueId: "ba-session" },
"sign-out",
)
const signOut = deployment.seen.findIndex((call) => call.url === "http://site.test/api/auth/sign-out")
expect(signOut).toBeGreaterThan(0)
expect(new Headers(deployment.seen[signOut]?.init?.headers).get("origin")).toBe("http://app.test")
expect(response.status).toBe(303)
expect(response.headers.get("location")).toBe("http://app.test/auth/signed-out")
expect(response.headers.getSetCookie()).toContain(
"better-auth.session_token=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax",
)
} finally {
await runtime.stop()
}
})
it("clears the cookies of a browser whose session Better Auth cannot confirm, and says so", async () => {
const { runtime, onSessionRevoked, warnings } = runtimeWith()
try {
const response = await runtime.signOut(signedIn(""))
expect(onSessionRevoked).not.toHaveBeenCalled()
expect(response.status).toBe(303)
expect(response.headers.getSetCookie().length).toBeGreaterThan(0)
expect(warnings).toEqual([])
} finally {
await runtime.stop()
}
})
it("ends a Better Auth account with the deployment's admin key, by its Better Auth user ID", async () => {
const { runtime, deployment } = runtimeWith()
try {
await runtime.endAccount("ba-user")
const call = deployment.seen.find((candidate) => candidate.url.startsWith("http://cloud.test/api/mutation"))
expect(JSON.parse(String(call?.init?.body))).toMatchObject({
path: "conusaiAuth:endAccount",
args: [{ userId: "ba-user" }],
})
expect(new Headers(call?.init?.headers).get("authorization")).toBe("Convex admin-key-of-the-test")
} finally {
await runtime.stop()
}
})
})
describe("the account deletion's Better Auth step, bound in every process that may run it", () => {
const BOUND = {
AUTH_PROVIDER: "convex-better-auth",
AUTH_IDENTITY_NAMESPACE: "better-auth:test",
BETTER_AUTH_CONVEX_URL: "http://cloud.test",
BETTER_AUTH_CONVEX_ADMIN_KEY: "admin-key-of-the-test",
}
const providerOf = (container: Container) =>
container.has(ACCOUNT_IDENTITY_PROVIDER) ? container.resolve(ACCOUNT_IDENTITY_PROVIDER) : null
it("the web process binds the provider the ledger records, under the namespace; an OIDC app binds none", () => {
const web = new Container()
bindBetterAuthAccountEnd(web, BOUND)
expect([providerOf(web)?.id, providerOf(web)?.issuer]).toEqual(["convex-better-auth", "better-auth:test"])
const oidc = new Container()
bindBetterAuthAccountEnd(oidc, { ...BOUND, AUTH_PROVIDER: "oidc" })
expect(providerOf(oidc)).toBeNull()
})
it("the worker binds the same for its sweep, and refuses to start without the deployment's admin key", async () => {
const platform = await startFakePlatform()
try {
const env = {
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",
}
const registry = buildRegistry([...appModules])
const clients = platformClients(loadAppEnv("worker", env), registry, env)
// The worker reads its process environment, as it does in a container.
const bound = (extra: Record<string, string>) => {
for (const [name, value] of Object.entries(extra)) vi.stubEnv(name, value)
try {
const container = new Container()
bindModulePorts(container, clients, registry)
return container
} finally {
vi.unstubAllEnvs()
}
}
const worker = bound(BOUND)
expect([providerOf(worker)?.id, providerOf(worker)?.issuer]).toEqual(["convex-better-auth", "better-auth:test"])
expect(() => bound({ ...BOUND, BETTER_AUTH_CONVEX_ADMIN_KEY: "" })).toThrow("BETTER_AUTH_CONVEX_ADMIN_KEY")
expect(providerOf(bound({ AUTH_PROVIDER: "" }))).toBeNull()
} finally {
await platform.close()
}
})
})
describe("the Better Auth deployment's credentials stay server-side (no platform link injects them)", () => {
type Compose = { services: Record<string, { build?: { args?: unknown }; environment?: Record<string, string> }> }
const compose = (path: string) => parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as Compose
const holders = (file: Compose, name: string) =>
Object.entries(file.services)
.filter(([, service]) => service.environment?.[name] !== undefined)
.map(([id]) => id)
.sort()
it("names the admin key only in the server processes that use it, each from the deployment's own variable", () => {
const postgres = compose("compose.yaml")
const convex = compose("profiles/convex/compose.yaml")
// The web server (session checks, the relay), the worker (the sweep ends a deleted account), the
// job that deploys the Better Auth functions; on Convex the deploy job also installs it in the
// app's backend, whose account actions end the account there.
expect(holders(postgres, "BETTER_AUTH_CONVEX_ADMIN_KEY")).toEqual(["migrate", "web", "worker"])
expect(holders(convex, "BETTER_AUTH_CONVEX_ADMIN_KEY")).toEqual(["convex-deploy", "web"])
// The secret: the deploy job alone.
expect(holders(postgres, "BETTER_AUTH_SECRET")).toEqual(["migrate"])
expect(holders(convex, "BETTER_AUTH_SECRET")).toEqual(["convex-deploy"])
for (const file of [postgres, convex]) {
for (const [id, service] of Object.entries(file.services)) {
for (const [name, value] of Object.entries(service.environment ?? {})) {
if (!name.startsWith("BETTER_AUTH_")) continue
// Passed through from the variable of the same name, defaulting to empty: never a literal.
expect(value, `${id}: ${name}`).toBe(`\${${name}:-}`)
}
expect(JSON.stringify(service.build?.args ?? {}), `${id} builds with no Better Auth value`).not.toContain(
"BETTER_AUTH",
)
// `VITE_` is the prefix Vite writes into the browser's bundle (test/asset-config.test.ts keeps it the only one).
expect(Object.keys(service.environment ?? {}).filter((name) => name.startsWith("VITE_"))).toEqual([])
}
}
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
expect(dockerfile).not.toContain("BETTER_AUTH")
const example = readFileSync(new URL("../.env.example", import.meta.url), "utf8")
expect(example).not.toMatch(/VITE_[A-Z_]*BETTER_AUTH|VITE_[A-Z_]*CONVEX/)
})
it("links the Better Auth deployment only as the named better-auth Convex link, never beside Identity", () => {
// A link role decides the injected names (BL): the default Convex link injects
// CONVEX_SELF_HOSTED_*, the better-auth one BETTER_AUTH_CONVEX_* as runtime-only variables.
// `kit template:prune --auth convex-better-auth` swaps the Identity link for it; an app that
// keeps OIDC declares none (docs/how-to/build-an-app-on-the-template.md).
for (const path of ["conusai.json", "profiles/convex/conusai.json"]) {
const config = JSON.parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as {
links?: Array<{ type: string; role?: string }>
}
const links = config.links ?? []
const roles = links.filter((link) => link.type === "convex").map((link) => link.role ?? "default")
expect(new Set(roles).size, `${path}: one Convex link per role`).toBe(roles.length)
const betterAuth = links.filter((link) => link.role === "better-auth")
expect(
betterAuth.every((link) => link.type === "convex"),
path,
).toBe(true)
if (betterAuth.length > 0) {
expect(
links.some((link) => link.type === "identity"),
`${path}: no Identity link beside Better Auth`,
).toBe(false)
}
}
})
})
describe("Better Auth's account events", () => {
it("go to the account module's flow as the mapped app user; nothing for a subject never mapped", async () => {
const delivered: unknown[] = []
const ports = { accountFlow: async (event: unknown) => void delivered.push(event) }
const event = {
reason: "user-disabled",
identityNamespace: "better-auth:test",
providerSubject: "ba-user",
} as const
await deliverAccountEvent(ports, { ...event, kitUserId: null })
expect(delivered).toEqual([])
await deliverAccountEvent(ports, { ...event, kitUserId: "app-user" })
await deliverAccountEvent(ports, { ...event, reason: "account-deleted", kitUserId: "app-user" })
expect(delivered).toEqual([
{ reason: "user-disabled", userId: "app-user" },
{ reason: "account-deleted", userId: "app-user" },
])
})
it("throw the flow's failure, so the relay keeps the event and delivers it again", async () => {
const ports = {
accountFlow: async () => {
throw new Error("backend unavailable")
},
}
await expect(
deliverAccountEvent(ports, {
reason: "account-deleted",
identityNamespace: "better-auth:test",
providerSubject: "ba-user",
kitUserId: "app-user",
}),
).rejects.toThrow("backend unavailable")
})
})
describe("the sign-in form", () => {
it("checks the password length Better Auth enforces, and labels every error from a fixed set", () => {
expect(MIN_PASSWORD_LENGTH).toBe(OPTIONS_MIN)
const labels = betterAuthFormLabels(
{ message: (key, values) => `${key}${values ? JSON.stringify(values) : ""}` },
"sign-up",
)
expect(labels.passwordHelp).toBe(`signin.account.passwordHelp{"min":${MIN_PASSWORD_LENGTH}}`)
expect(Object.keys(labels.errors).sort()).toEqual([
"disabled",
"exists",
"failed",
"invalid",
"rateLimited",
"unavailable",
])
})
})
describe("the locally installed Better Auth component", () => {
it("is the schema the app's Better Auth options generate, with the admin plugin's fields", async () => {
const options = {
database: convexAdapter({} as never, {} as never),
...betterAuthOptions({ providers: [{ applicationID: "convex", domain: "" }] }),
}
const adapter = (
options.database as unknown as (all: unknown) => {
createSchema(all: unknown, file: string): Promise<{ code: string }>
}
)(options)
const { code } = await adapter.createSchema(options, "better-auth/functions/betterAuth/schema.ts")
const body = code.slice(code.indexOf("import "))
const header = `// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Generated from ./options.ts by the @convex-dev/better-auth schema generator: do not edit it by hand.
// After a change of the Better Auth options, regenerate it with
// pnpm exec vitest run test/better-auth.test.ts -u
// (the "local install" of @convex-dev/better-auth, which is how the admin plugin's fields get here).
`
for (const field of ["role:", "banned:", "banReason:", "banExpires:", "impersonatedBy:"])
expect(body).toContain(field)
await expect(`${header}${body}`).toMatchFileSnapshot("../better-auth/functions/betterAuth/schema.ts")
})
it("maps one Better Auth user to one app user, under the configured namespace and never by e-mail", async () => {
const registry = buildRegistry([tenancyModule])
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const first = await requireAppUser(data, {
issuer: config.identityNamespace,
subject: "ba-user",
email: "ada@example.test",
displayName: "Ada",
locale: "en",
})
const other = await requireAppUser(data, {
issuer: config.identityNamespace,
subject: "ba-other",
email: "ada@example.test",
displayName: "Ada",
locale: "en",
})
expect(other.id).not.toBe(first.id)
})
})
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The boundary rules are generated from the folder tree (spec §2), so a module added tomorrow is
// covered without anybody editing a list. This suite is that promise.
import { createRequire } from "node:module"
import { describe, expect, it } from "vitest"
const require = createRequire(import.meta.url)
const { buildForbidden, listModules } = require("../scripts/dep-rules.cjs") as {
buildForbidden(
moduleIds: string[],
): { name: string; from: { path?: string; pathNot?: string }; to: { path?: string | string[]; circular?: boolean } }[]
listModules(root: string): string[]
}
const named = (rules: ReturnType<typeof buildForbidden>, name: string) => rules.find((rule) => rule.name === name)
describe("the generated boundary rules", () => {
it("find this app's modules on disk", () => {
const found = listModules(new URL("..", import.meta.url).pathname)
expect(Array.isArray(found)).toBe(true)
})
it("keep the worker entry out of every app but the worker", () => {
const rule = named(buildForbidden([]), "worker-entry-is-the-workers-own")
expect(rule?.from.pathNot).toBe("^apps/worker/")
// The resolved path, which is what dependency-cruiser matches on.
expect(rule?.to.path).toBe("@conusai-kit/core/dist/worker\\.")
})
it("keep a driver, an SDK and a framework out of a module's core", () => {
const rule = named(buildForbidden(["notes"]), "core-imports-no-engine-sdk-or-framework")
const patterns = (rule?.to.path ?? []) as string[]
const forbidden = (name: string): boolean => patterns.some((pattern) => new RegExp(pattern).test(name))
expect(forbidden("kysely")).toBe(true)
expect(forbidden("react")).toBe(true)
// The web shell's framework, by the names a use case could import it under.
expect(forbidden("@tanstack/react-start")).toBe(true)
expect(forbidden("@tanstack/react-start/server")).toBe(true)
expect(forbidden("@tanstack/react-router")).toBe(true)
expect(forbidden("@conusai-sdk/blob")).toBe(true)
// And the contracts a use case is written against stay open.
expect(forbidden("@conusai-kit/core")).toBe(false)
})
it("add a rule per module pair, so a module reaches another only through its contract", () => {
const rules = buildForbidden(["documents", "notes"])
expect(named(rules, "module-notes-crosses-only-contracts")?.to.path).toBe("^modules/(documents)/(?!contract/)")
expect(named(rules, "module-documents-crosses-only-contracts")?.to.path).toBe("^modules/(notes)/(?!contract/)")
})
it("add no module rule when there is only one module to cross into", () => {
expect(buildForbidden(["notes"]).some((rule) => rule.name.startsWith("module-"))).toBe(false)
})
it("refuse a cycle anywhere", () => {
expect(named(buildForbidden([]), "no-circular")?.to.circular).toBe(true)
})
})
+133
View File
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The brand is the only source of colour (decision AT-D11), and every string a page shows is a
// message in both locales (spec §10.4).
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it, vi } from "vitest"
import { appCatalogues } from "../apps/web/src/ui/messages"
import { appBrand, BRAND_TOKEN_NAMES, brandTokenStyle } from "../config/src/brand.js"
const walk = (dir: string, suffix: RegExp): string[] =>
readdirSync(dir).flatMap((name) => {
// Installed packages are not this app's pages, and a linked workspace package would pull its
// own sources (stories, fixtures) into the scan. What a build or the Start plugin writes is not source.
if (["node_modules", "dist", ".tanstack"].includes(name)) return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path, suffix)
return suffix.test(path) ? [path] : []
})
const appDir = new URL("..", import.meta.url).pathname
describe("the app brand", () => {
it("reads its tokens from @conusai-kit/brand, so the app defines none", () => {
expect(BRAND_TOKEN_NAMES.length).toBeGreaterThan(0)
expect(appBrand.tokens).toEqual({})
expect(brandTokenStyle({ ...appBrand, tokens: { primary: "#123456" } })).toEqual({ "--brand-primary": "#123456" })
})
it("ships no colour literal of its own in the stylesheet", () => {
const css = readFileSync(join(appDir, "apps/web/src/ui/globals.css"), "utf8")
const literals = css.match(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/g) ?? []
expect(literals).toEqual([])
})
it("loads no external stylesheet, font or script", () => {
// The document's head is `src/ui/document.tsx` and the root route's `head`, not only the pages.
const files = walk(join(appDir, "apps/web/src"), /\.(?:tsx|css)$/)
expect(files.some((file) => file.endsWith("src/routes/__root.tsx"))).toBe(true)
expect(files.some((file) => file.endsWith("src/ui/document.tsx"))).toBe(true)
expect(files.some((file) => file.endsWith("src/ui/globals.css"))).toBe(true)
for (const file of files) {
const text = readFileSync(file, "utf8")
expect(text, `${file} reaches outside this app`).not.toMatch(/https?:\/\/(?!127\.0\.0\.1|localhost)/)
}
})
it("has no attribution turned on by default: a customer sells their own product", () => {
expect(appBrand.attribution).toBe(false)
})
})
describe("the message catalogues", () => {
const en = appCatalogues.en ?? {}
const lt = appCatalogues.lt ?? {}
it("hold the same keys in English and in Lithuanian", () => {
for (const [module, messages] of Object.entries(en)) {
expect(Object.keys(lt[module] ?? {}).sort(), `lt is missing keys of ${module}`).toEqual(
Object.keys(messages).sort(),
)
}
})
it("select the Lithuanian plural categories, which English does not have", () => {
expect(lt.workspace?.projectCount).toContain("few {")
expect(en.workspace?.projectCount).not.toContain("few {")
})
it("name no role of their own: a role's one name is the kit's, on every page", () => {
// The invitation form, the invitation page, the members table and the role picker all read
// `builtInRoleName` and `roleName` of @conusai-kit/tenancy/ui, so a member is the same Editor on each.
expect(en.roles).toBeUndefined()
expect(lt.roles).toBeUndefined()
for (const file of ["../apps/web/src/server/org-pages.ts", "../apps/web/src/server/account-pages.ts"])
expect(readFileSync(new URL(file, import.meta.url), "utf8"), file).not.toMatch(/message\(`roles\./)
})
it("hold the messages a page picks by a value rather than by name: sign-in errors, invitation states", () => {
const keys = [
...["sessionExpired", "noTransaction", "denied", "provider", "failed"].map((code) => `signin.error.${code}`),
...["invalid", "used", "expired", "member", "failed"].map((state) => `invite.error.${state}`),
]
for (const key of keys) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `${key} is not translated`).not.toBe(en[module as string]?.[message])
}
})
it("select the Lithuanian plural categories of the counts the workspace pages show", () => {
expect(lt.workspace?.projectCount).toContain("few {")
expect(en.workspace?.projectCount).not.toContain("few {")
})
it("hold every message the pages ask for, in both locales", () => {
// Every label a page shows is `i18n.message("<key>")`, so the keys the pages use and the keys
// the catalogues hold are comparable -- and a renamed message fails here rather than
// rendering its own key to a customer. A loader builds a page's labels on the server (F10), so the
// keys are in `.ts` files as well as `.tsx`, often through a local `message` over `i18n.message`.
const used = new Set<string>()
for (const file of walk(join(appDir, "apps/web"), /\.tsx?$/)) {
for (const [, key] of readFileSync(file, "utf8").matchAll(/\bmessage\("([^"]+)"/g)) used.add(key as string)
}
// The shell's labels, read from `shellData()`: a walk that missed loaders would stop finding these.
expect(used).toContain("app.signIn")
expect(used).toContain("boundary.notFoundTitle")
// And the pages' own: `src/server/account-pages.ts` and `src/server/org-pages.ts`.
expect(used).toContain("app.organizations")
expect(used).toContain("workspace.create")
expect(used.size).toBeGreaterThan(10)
for (const key of [...used].sort()) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
}
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules([
"documents",
"knowledge",
"agent",
"guides",
"support",
]),
}))
+116
View File
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What a page says about itself, rendered with a customer's brand: their name, their colours on the
// first byte of the document, and no trace of ConusAI's (plan item AT2.6 done_when, the rendered-page
// half; there are no emails or share images in the template to render).
import { brandTokenStyle, defineBrand } from "@app/config"
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { brandHead, brandMetadata, brandTokensOnly, Document } from "../apps/web/src/ui/document"
import { appCatalogues } from "../apps/web/src/ui/messages"
const customer = defineBrand({
name: "Acme Review",
shortName: "Acme",
primary: "#1a1a8c",
logo: { light: "/acme-light.svg", dark: "/acme-dark.svg", favicon: "/acme.svg" },
})
// The layout's own consent wording in the page's locale, as RootLayout passes it.
const consent = (key: string): string => {
const text = appCatalogues.lt?.consent?.[key]
if (!text) throw new Error(`missing lt consent.${key}`)
return text
}
const privacy = {
title: consent("title"),
body: consent("body"),
replay: consent("replay"),
accept: consent("accept"),
reject: consent("reject"),
reopen: consent("reopen"),
gpc: consent("gpc"),
}
const render = (extra: { theme?: "dark" | "light"; observe?: { release: string; environment: string } } = {}): string =>
renderToStaticMarkup(
createElement(Document, {
brand: { ...customer, style: brandTokenStyle(customer) },
locale: "lt",
theme: extra.theme ?? "dark",
signedIn: false,
signIn: "Prisijungti",
signOut: null,
controls: null,
privacy,
...(extra.observe ? { observe: extra.observe } : {}),
children: createElement("main", null, "turinys"),
}),
)
const html = render()
describe("a page rendered with a customer's brand", () => {
it("carries the customer's name and logos and no ConusAI name", () => {
expect(html).toContain("Acme Review")
expect(html).toContain('src="/acme-dark.svg"')
expect(html.toLowerCase()).not.toContain("conusai")
})
it("writes the brand layer as a stylesheet in the head, so the first paint is branded and a platform theme can win", () => {
const root = /<html[^>]*>/.exec(html)?.[0] ?? ""
expect(root).toContain('lang="lt"')
expect(root).not.toContain("--primary")
const layer = /<style data-brand-layer="app"[^>]*>([^<]*)<\/style>/.exec(html)?.[1] ?? ""
expect(html.indexOf("data-brand-layer")).toBeLessThan(html.indexOf("</head>"))
expect(layer).toMatch(/^:root:root\{.*--primary:light-dark\(#1a1a8c, ?#[0-9a-f]{6}\)/)
expect(layer).toMatch(/--ring:light-dark\(#[0-9a-f]{6}, ?#[0-9a-f]{6}\)/)
})
it("leaves the colours to the platform's organization theme when there is one, keeping the brand's own tokens", () => {
expect(brandTokensOnly({ "--primary": "#1a1a8c", "--brand-font-size-root": "16px" })).toEqual({
"--brand-font-size-root": "16px",
})
})
it("names the customer, and only the customer, in the tab and in a share card", () => {
const metadata = JSON.stringify(brandMetadata(customer))
expect(metadata).toContain("Acme Review")
expect(metadata.toLowerCase()).not.toContain("conusai")
})
it("points the share card at the app's own image, or at the organization's on its own pages", () => {
const app = brandMetadata(customer)
expect(app.openGraph?.images).toEqual([{ url: "/api/brand/og", width: 1200, height: 630, alt: "Acme Review" }])
expect(app.twitter).toMatchObject({ card: "summary_large_image", images: ["/api/brand/og"] })
const organization = brandMetadata(customer, "/acme/brand/og")
expect(JSON.stringify(organization)).toContain("/acme/brand/og")
expect(JSON.stringify(organization)).not.toContain("/api/brand/og")
})
it("gives a link preview an absolute image address from the deployment's public URL", () => {
const { meta } = brandHead(brandMetadata(customer), "https://app.example.test")
expect(meta).toContainEqual({ property: "og:image", content: "https://app.example.test/api/brand/og" })
expect(meta).toContainEqual({ name: "twitter:image", content: "https://app.example.test/api/brand/og" })
})
it("has exactly one element that carries the theme class: the page, not a component", () => {
expect((html.match(/class="[^"]*\blight\b/g) ?? []).length).toBeLessThanOrEqual(1)
})
it("puts the light theme on the root element from the first byte, and the dark one on nothing", () => {
expect(/<html[^>]*>/.exec(html)?.[0]).not.toMatch(/class="[^"]*\blight\b/)
const light = render({ theme: "light" })
expect(/<html[^>]*>/.exec(light)?.[0]).toMatch(/class="light"/)
expect((light.match(/class="(?:[^"]* )?light(?: [^"]*)?"/g) ?? []).length).toBe(1)
})
it("tells the browser to start observability only when the server turned it on, without naming ConusAI", () => {
expect(html).not.toContain("app-observe")
const observed = render({ observe: { release: "2026.10.1", environment: "production" } })
expect(observed).toContain('name="app-observe"')
expect(observed).toContain('data-release="2026.10.1"')
expect(observed.toLowerCase()).not.toContain("conusai")
})
})
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The app brand (plan item AT2.6). A customer replaces the ConusAI defaults in config/src/brand.ts with
// `defineBrand`, and what they give it has to be safe to write into a stylesheet and a page. The
// legibility of their colours is @conusai-kit/brand's own test: this file checks what the app does with them.
import { appBrand, brandContrast, brandTokenStyle, defineBrand } from "@app/config"
import { describe, expect, it } from "vitest"
const base = { name: "Acme Review", shortName: "Acme" }
describe("defineBrand", () => {
it("returns the brand, frozen, with its colours as a style", () => {
const brand = defineBrand({ ...base, primary: "#1a1a8c" })
expect(brand.name).toBe("Acme Review")
expect(Object.isFrozen(brand)).toBe(true)
expect(brand.attribution).toBe(false)
// The light page keeps the brand's colour; the dark page gets it lifted until a button stands out (kit-brand).
expect(brandTokenStyle(brand)["--primary"]).toMatch(/^light-dark\(#1a1a8c, #[0-9a-f]{6}\)$/)
expect(brandTokenStyle(brand)["--sidebar-primary"]).toBe(brandTokenStyle(brand)["--primary"])
expect(Object.keys(brandTokenStyle(brand)).some((name) => name.startsWith("--brand-accent"))).toBe(false)
})
it("reports the contrast its colours have on each theme", () => {
const report = brandContrast(defineBrand({ ...base, primary: "#1a1a8c" }))
expect(report.dark?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(report.light?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(brandContrast(defineBrand(base))).toEqual({})
})
it("refuses a colour that is not hex, naming the field and not the value", () => {
expect(() => defineBrand({ ...base, primary: "red" })).toThrow(/primary/)
expect(() => defineBrand({ ...base, primary: "secret-colour" })).not.toThrow(/secret-colour/)
expect(() => defineBrand({ ...base, accent: "#12" })).toThrow(/accent/)
})
it("refuses a name that is empty, too long or carries a control character", () => {
for (const name of ["", " ", "x".repeat(61), "bad\u0007name"]) {
expect(() => defineBrand({ name, shortName: "x" })).toThrow(/name/)
}
})
it("refuses a logo that is not a path on this app's own origin", () => {
for (const logo of [
"https://cdn.example.test/logo.svg",
"//cdn.example.test/logo.svg",
"javascript:alert(1)",
"data:image/svg+xml,<svg/>",
"logo.svg",
]) {
expect(() => defineBrand({ ...base, logo: { light: logo, dark: "/a.svg", favicon: "/a.svg" } }), logo).toThrow(
/logo/,
)
}
expect(defineBrand({ ...base, logo: { light: "/l.svg", dark: "/d.svg", favicon: "/f.svg" } }).logo.dark).toBe(
"/d.svg",
)
})
it("refuses a token that could break out of a style attribute", () => {
for (const value of ["red; background: url(//evil)", "#fff}", "url(//evil)", "expression(1)", "a\\b", "<b>"]) {
expect(() => defineBrand({ ...base, tokens: { "--brand-accent": value } }), value).toThrow(/token/)
}
expect(() => defineBrand({ ...base, tokens: { "not a name": "#fff" } })).toThrow(/token/)
expect(brandTokenStyle(defineBrand({ ...base, tokens: { "radius-panel": "0.5rem" } }))).toEqual({
"--brand-radius-panel": "0.5rem",
})
})
it("is what the app ships: a neutral name, no ConusAI wording, no attribution", () => {
expect(JSON.stringify(appBrand).toLowerCase()).not.toContain("conusai")
expect(appBrand.attribution).toBe(false)
})
})
+329
View File
@@ -0,0 +1,329 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The tenant brand end to end (plan item AT2.6): who may change it, what is refused, what is stored,
// and what the organization layout will read back. Every call goes through the kernel's caller, so
// the permission check, the validation and the data wall are the real ones.
import { type BrandView, tenantStyle } from "@app/modules/branding"
import { checkLogo } from "@conusai-kit/brand/logo/check"
import { AppError } from "@conusai-kit/core"
import { organizations } from "@conusai-kit/tenancy"
import { afterEach, describe, expect, it } from "vitest"
import { closeApps, type DocumentsApp, makeDocumentsApp, type SeededProject } from "./support/documents-app.js"
import { chunk, dimensions, MARK_WEBP, markPng, noisePng } from "./support/images.js"
afterEach(closeApps)
const document = { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] }
async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> {
const app = await makeDocumentsApp({ document })
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
// Real images: the kit's checks read a PNG's chunks and decode its pixels.
const PNG = markPng(1000, 250)
const png = (width: number) => markPng(width, 60)
const svgOf = (inner: string) => Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4">${inner}</svg>`)
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
const stored = (app: DocumentsApp, project: SeededProject) =>
app.data
.global(organizations)
.get(project.orgId)
.then((row) => row?.branding ?? null)
describe("reading the brand", () => {
it("answers nothing set for an organization that has no brand", async () => {
const { project } = await fresh()
expect(await project.call.callByName("branding.brand.get", { org: project.orgSlug })).toEqual({
displayName: null,
primary: null,
accent: null,
logoLight: null,
logoDark: null,
})
})
it("lets any member read it", async () => {
const { app, project } = await fresh()
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
expect(await viewer.call.callByName("branding.brand.get", { org: project.orgSlug })).toMatchObject({
primary: null,
})
})
})
describe("changing the brand", () => {
it("stores the name and the colours on the organization and answers with them", async () => {
const { app, project } = await fresh()
const view = await project.call.callByName("branding.brand.set", {
org: project.orgSlug,
displayName: " Acme Review ",
primary: "#1a1a8c",
accent: "#f4c430",
})
expect(view).toMatchObject({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" })
expect(await stored(app, project)).toEqual({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" })
})
it("leaves what is not sent as it was, and clears what is sent as null", async () => {
const { app, project } = await fresh()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c", accent: "#f4c430" })
await project.call.callByName("branding.brand.set", { org: project.orgSlug, accent: null })
expect(await stored(app, project)).toEqual({ primary: "#1a1a8c" })
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: null })
// an empty brand is no brand: one representation of "none"
expect(await stored(app, project)).toBeNull()
})
it("refuses a colour that is not hex, with the field named and the value not repeated", async () => {
const { app, project } = await fresh()
for (const bad of ["red", "#12", "#gggggg", "rgb(1,2,3)", "#fff;}body{display:none", "url(//evil)"]) {
const error = await refusal(project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: bad }))
expect(error.status, bad).toBe(422)
// the refusal names the field and does not repeat what was typed in it
expect(JSON.stringify(error.problem), bad).toContain("primary")
expect(JSON.stringify(error.problem), bad).not.toContain(bad)
}
expect(await stored(app, project)).toBeNull()
})
it("refuses an empty display name, which would be a name nobody sees", async () => {
const { project } = await fresh()
const error = await refusal(
project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: " " }),
)
expect(error.status).toBe(422)
})
it("is an administrator's act: a member and a viewer are answered as strangers, and nothing changes", async () => {
const { app, project } = await fresh()
const admin = await app.addMember(project, "admin@acme.test", "admin")
const member = await app.addMember(project, "member@acme.test", "member")
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
for (const who of [member, viewer]) {
// 404, not 403: the kernel answers a member who lacks the permission as it answers a stranger (spec §3.3)
expect(
(await refusal(who.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" }))).status,
).toBe(404)
expect(
(
await refusal(
who.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant: "light",
contentType: "image/png",
body: PNG.toString("base64"),
}),
)
).status,
).toBe(404)
}
expect(await stored(app, project)).toBeNull()
await admin.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" })
expect(await stored(app, project)).toEqual({ primary: "#000000" })
})
it("is declared to write one audit event per change", async () => {
const app = await makeDocumentsApp({ document })
expect(app.registry.procedures.get("branding.brand.set")?.def.audit).toBe("branding.brand_changed")
expect(app.registry.procedures.get("branding.logo.set")?.def.audit).toBe("branding.brand_changed")
expect(app.registry.audit["branding.brand_changed"]).toBeDefined()
})
})
const logoKeys = (app: DocumentsApp, project: SeededProject) =>
app.store
.keysOf({ level: "org", actor: project.owner, orgId: project.orgId })
.filter((key) => key.startsWith("branding/"))
.sort()
const setLogo = (project: SeededProject, variant: "light" | "dark", contentType: string, bytes: Uint8Array) =>
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant,
contentType,
body: Buffer.from(bytes).toString("base64"),
}) as Promise<BrandView>
const getLogo = async (project: SeededProject, variant: "light" | "dark", size?: "1x" | "2x") => {
const back = (await project.call.callByName("branding.logo.get", {
org: project.orgSlug,
variant,
...(size === undefined ? {} : { size }),
})) as { contentType: string; body: string }
return { contentType: back.contentType, bytes: Buffer.from(back.body, "base64") }
}
describe("the logos", () => {
it("stores a raster logo as its 1x and 2x renditions, and serves the 2x unless the 1x is asked for", async () => {
const { app, project } = await fresh()
const view = await setLogo(project, "light", "image/png", PNG)
expect(view.logoLight).toMatch(/^\/acme\/brand\/logo\/light\?v=[0-9a-f]{16}$/)
expect(view.logoDark).toBeNull()
const two = await getLogo(project, "light")
expect(two.contentType).toBe("image/png")
expect(dimensions(two.bytes)).toEqual({ width: 840, height: 210 })
expect(dimensions((await getLogo(project, "light", "1x")).bytes)).toEqual({ width: 420, height: 105 })
expect(two.bytes.equals(PNG)).toBe(false)
// the object lives under the organization's own namespace of the store: two files, never the upload
expect(logoKeys(app, project)).toHaveLength(2)
})
it("resizes an image larger than a stored logo may be instead of refusing it, and every stored file passes the kit check", async () => {
const { project } = await fresh()
const big = noisePng(700, 300)
expect(big.length).toBeGreaterThan(256 * 1024)
await setLogo(project, "dark", "image/png", big)
for (const size of ["1x", "2x"] as const) {
const { bytes, contentType } = await getLogo(project, "dark", size)
expect(bytes.length).toBeLessThanOrEqual(256 * 1024)
expect(checkLogo(contentType, bytes)).toMatchObject({ ok: true })
}
})
it("takes a WebP and stores it as PNG", async () => {
const { project } = await fresh()
await setLogo(project, "dark", "image/webp", MARK_WEBP)
const { bytes, contentType } = await getLogo(project, "dark")
expect(contentType).toBe("image/png")
expect(dimensions(bytes)).toEqual({ width: 400, height: 100 })
})
it("replaces a logo and removes the files it replaced", async () => {
const { app, project } = await fresh()
await setLogo(project, "dark", "image/png", png(100))
const second = await setLogo(project, "dark", "image/png", png(200))
const keys = logoKeys(app, project)
expect(keys).toHaveLength(2)
expect(keys.every((key) => second.logoDark?.includes(key.split("-")[2] ?? "?"))).toBe(true)
// the same image again is the same logo: nothing is lost
const third = await setLogo(project, "dark", "image/png", png(200))
expect(third.logoDark).toBe(second.logoDark)
expect(logoKeys(app, project)).toEqual(keys)
})
it("keeps an SVG that is shapes alone exactly as uploaded, one file for both sizes", async () => {
const { app, project } = await fresh()
const logo = svgOf('<rect width="4" height="4" fill="#123456"/>')
const view = await setLogo(project, "light", "image/svg+xml", logo)
expect(view.logoLight).not.toBeNull()
expect(logoKeys(app, project)).toHaveLength(1)
for (const size of [undefined, "1x", "2x"] as const) {
const back = await getLogo(project, "light", size)
expect(back.contentType).toBe("image/svg+xml")
expect(back.bytes.equals(logo)).toBe(true)
}
})
it("refuses an SVG that carries a script, stores nothing and leaves the brand as it was", async () => {
const { app, project } = await fresh()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c" })
const before = await stored(app, project)
for (const inner of [
"<script>alert(1)</script>",
'<rect onload="alert(1)" width="1" height="1"/>',
'<image href="https://evil.example.test/x.png"/>',
]) {
const error = await refusal(setLogo(project, "light", "image/svg+xml", svgOf(inner)))
expect(error.status, inner).toBe(422)
}
expect(await stored(app, project)).toEqual(before)
expect(logoKeys(app, project)).toEqual([])
})
it("refuses a file the kit's checks refuse, naming the rule and not the file", async () => {
const { app, project } = await fresh()
const damaged = Buffer.from(PNG)
damaged[40] = (damaged[40] ?? 0) ^ 0xff
const lie = Buffer.concat([
PNG.subarray(0, 8),
PNG.subarray(8, 33),
chunk("IDAT", Buffer.from("not zlib data at all")),
chunk("IEND", new Uint8Array()),
])
for (const [name, type, bytes, rule] of [
["a PNG that is really markup", "image/png", Buffer.from("<svg/>"), /not a PNG/],
["a WebP that is a PNG", "image/webp", PNG, /not a WebP/],
["a PNG with a damaged chunk", "image/png", damaged, /damaged/],
["a PNG cut short", "image/png", PNG.subarray(0, 200), /damaged|PNG/],
["a PNG whose data does not decode", "image/png", lie, /decoded|visible/],
["a type that is not an image", "text/html", Buffer.from("<html>"), /PNG, WebP or SVG/],
[
"more than the upload limit",
"image/png",
Buffer.concat([PNG, Buffer.alloc(2 * 1024 * 1024 + 1 - PNG.length)]),
/larger than/,
],
] as const) {
const error = await refusal(setLogo(project, "light", type, bytes))
expect(error.status, name).toBe(422)
expect(JSON.stringify(error.problem), name).toMatch(rule)
}
expect(await stored(app, project)).toBeNull()
expect(logoKeys(app, project)).toEqual([])
})
it("refuses a body that is not base64", async () => {
const { project } = await fresh()
const text = await refusal(
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant: "light",
contentType: "image/png",
body: "not base64!",
}),
)
expect(text.status).toBe(422)
})
it("is removed from the brand and from the store when it is cleared, and then there is no logo to read", async () => {
const { app, project } = await fresh()
await setLogo(project, "light", "image/png", PNG)
await setLogo(project, "dark", "image/png", png(100))
expect(logoKeys(app, project)).toHaveLength(4)
await project.call.callByName("branding.logo.clear", { org: project.orgSlug, variant: "light" })
expect(logoKeys(app, project)).toHaveLength(2)
expect(
(await refusal(project.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status,
).toBe(404)
expect(dimensions((await getLogo(project, "dark")).bytes)).toEqual({ width: 100, height: 60 })
})
it("is not readable by another organization's member", async () => {
const { app, project } = await fresh()
await setLogo(project, "light", "image/png", PNG)
const other = await app.seedProject("other", "owner@other.test")
expect(
(await refusal(other.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status,
).toBe(404)
})
})
describe("what the organization layout writes from a stored brand", () => {
it("is the brand's own colours, legible in both themes", () => {
const { style, report } = tenantStyle({ primary: "#1a1a8c", accent: "#f4c430" })
expect(style["--primary"]).toMatch(/^light-dark\(#1a1a8c, #[0-9a-f]{6}\)$/)
expect(style["--sidebar-primary"]).toBe(style["--primary"])
expect(style["--ring"]).toMatch(/^light-dark\(#1a1a8c, #[0-9a-f]{6}\)$/)
expect(style["--accent"]).toBe("color-mix(in oklab, #f4c430 18%, var(--background))")
expect(style["--primary-foreground"]).toMatch(/^#ffffff$|^light-dark\(#ffffff, #[0-9a-f]{6}\)$/)
expect(report.dark?.edgeContrast).toBeGreaterThanOrEqual(3)
expect(report.dark?.fillContrast).toBeGreaterThanOrEqual(4.5)
expect(report.dark?.inkContrast).toBeGreaterThanOrEqual(4.5)
expect(report.light?.inkContrast).toBeGreaterThanOrEqual(4.5)
})
it("is nothing for an organization with no colour of its own, so the app's brand shows", () => {
expect(tenantStyle({ primary: null, accent: null }).style).toEqual({})
})
})
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Deployment credentials and an unverified tuple cannot establish app authority.
import { BUSINESS_REQUEST_BINDING } from "@conusai-kit/business"
import { Container } from "@conusai-kit/core"
import { describe, expect, it, vi } from "vitest"
import { activitiesServiceOf } from "../modules/activities/adapters/service.js"
import { archiveServiceOf } from "../modules/archive/adapters/service.js"
import { catalogServiceOf } from "../modules/catalog/adapters/service.js"
import { crmServiceOf } from "../modules/crm/adapters/service.js"
import { inventoryServiceOf } from "../modules/inventory/adapters/service.js"
const env = {
CONUSAI_API_URL: "https://cloud.example.test/api",
CONUSAI_API_TOKEN: "synthetic-deployment-token",
CONUSAI_PROJECT_ID: "7",
CONUSAI_ORG_SLUG: "fixture",
}
describe("business service app partition admission", () => {
for (const [name, resolve] of Object.entries({
activities: activitiesServiceOf,
archive: archiveServiceOf,
catalog: catalogServiceOf,
crm: crmServiceOf,
inventory: inventoryServiceOf,
})) {
it(`${name} refuses an absent or unverified request binding before network dispatch`, () => {
for (const [key, value] of Object.entries(env)) vi.stubEnv(key, value)
const fetch = vi.spyOn(globalThis, "fetch")
try {
const unbound = new Container()
const forged = new Container()
forged.value(BUSINESS_REQUEST_BINDING, {
bindingId: "01900000-0000-7000-8000-000000000001",
orgSlug: "fixture",
client: () => {
throw new Error("unverified client must never be constructed")
},
} as never)
for (const container of [unbound, forged])
expect(() => resolve(container, "read")).toThrow(
expect.objectContaining({ code: "service_partition_missing", status: 409 }),
)
expect(fetch).not.toHaveBeenCalled()
} finally {
fetch.mockRestore()
vi.unstubAllEnvs()
}
})
}
})
+383
View File
@@ -0,0 +1,383 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Simulated native transport and actual template admission boundaries; no live readiness is asserted.
import * as nativeCore from "@conusai-kit/core" // kit:guides // kit:support
import { Container, type ServiceAdmissionContext } from "@conusai-kit/core"
import * as kitData from "@conusai-kit/data" // kit:guides // kit:support
import * as kitGuides from "@conusai-kit/guides" // kit:guides // kit:support
import * as kitSupport from "@conusai-kit/support" // kit:guides // kit:support
import * as kitTenancy from "@conusai-kit/tenancy" // kit:guides // kit:support
import { describe, expect, it, vi } from "vitest"
import { nativeHelpSelectors as webSelectors } from "../apps/web/src/server/ports.js" // kit:guides // kit:support
import { nativeHelpSelectors as workerSelectors } from "../apps/worker/src/ports.js" // kit:guides // kit:support
import { businessServiceComposition } from "../config/src/business-services.js"
import { crmModule } from "../modules/crm/module.js"
const env = {
deployment: {
CONUSAI_API_URL: "https://platform.example.test/api",
CONUSAI_API_TOKEN: "synthetic-token",
CONUSAI_PROJECT_ID: "9",
},
}
const context = {
container: new Container(),
module: "crm",
procedure: "crm.party.list",
kind: "read",
scope: {
level: "org",
orgId: "01900000-0000-7000-8000-000000000001",
actor: { userId: "01900000-0000-7000-8000-000000000002" },
},
} satisfies ServiceAdmissionContext
describe("optional business request composition", () => {
it("requires no additional deployment metadata when business modules are absent", () => {
expect(businessServiceComposition(env, [], "postgres")).toEqual({})
})
it("does not send native platform requirements to the business gateway", async () => {
const native = {
...crmModule,
serviceRequirements: crmModule.serviceRequirements.map((requirement) => ({
...requirement,
transport: "platform" as const,
})),
}
expect(businessServiceComposition(env, [native], "postgres").serviceReadiness).toBeDefined()
const hooks = businessServiceComposition(env, [crmModule], "postgres")
expect(native.serviceRequirements).toHaveLength(1)
for (const requirement of native.serviceRequirements)
await expect(hooks.serviceReadiness?.observe(requirement, context)).rejects.toMatchObject({
code: "service_observation_missing",
})
})
it("routes native observations through the exact environment and independently expected Cloud identity", async () => {
const descriptor = `sha256:${"a".repeat(64)}`
const source = "b".repeat(40)
const complete = {
deployment: {
...env.deployment,
CONUSAI_ORG_ID: "1",
CONUSAI_ENVIRONMENT_ID: "3",
CONUSAI_ORG_SLUG: "org",
CONUSAI_WORKSPACE_SLUG: "workspace",
CONUSAI_PROJECT_SLUG: "project",
CONUSAI_PLATFORM_RELEASE_DESCRIPTOR: descriptor,
CONUSAI_PLATFORM_SOURCE_COMMIT: source,
},
}
const baseRequirement = crmModule.serviceRequirements[0]
if (!baseRequirement) throw new Error("the CRM fixture must declare a service requirement")
const requirement = {
...baseRequirement,
transport: "platform" as const,
service: "ocr",
apiRange: "=1.0.0-preview.20261103",
partitionRequired: false,
operations: { "crm.party.list": ["ocr:use"] },
}
const module = { ...crmModule, serviceRequirements: [requirement] }
const fetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json({
component: {
id: "ocr",
observed_at: new Date().toISOString(),
facts: {
installed: "true",
enabled: "true",
entitled: "true",
configured: "true",
ready: "true",
},
},
authorized: "true",
environment_bound: "true",
compatible: "true",
dependencies_ready: "true",
policy_epoch: 2,
descriptor_digest: descriptor,
source_commit: source,
organization_id: 1,
project_id: 9,
environment_id: 3,
authorization_checked_at: new Date().toISOString(),
}),
)
try {
const hooks = businessServiceComposition(complete, [module], "postgres")
const projectContext = {
...context,
scope: {
level: "project" as const,
orgId: "01900000-0000-7000-8000-000000000001",
workspaceId: "local-workspace",
projectId: "local-project",
actor: { userId: "01900000-0000-7000-8000-000000000002" },
},
}
await hooks.prepareServices?.(new Container(), projectContext, [requirement])
expect(fetch).not.toHaveBeenCalled()
const observation = await hooks.serviceReadiness?.observe(requirement, projectContext)
expect(observation?.nativeAuthority?.expectedDescriptorDigest).toBe(descriptor)
expect(new URL(String(fetch.mock.calls[0]?.[0])).pathname).toBe(
"/api/v1/orgs/org/workspaces/workspace/projects/project/environments/3/platform-services/ocr/readiness",
)
expect(fetch).toHaveBeenCalledTimes(1)
} finally {
fetch.mockRestore()
}
})
it("constructs no provider or network work and leaves unrelated procedures usable", async () => {
const fetch = vi.spyOn(globalThis, "fetch")
try {
const hooks = businessServiceComposition(env, [crmModule], "postgres")
await hooks.prepareServices?.(
new Container(),
{ ...context, module: "tenancy", procedure: "tenancy.org.list" },
[],
)
expect(fetch).not.toHaveBeenCalled()
await expect(
hooks.prepareServices?.(new Container(), context, crmModule.serviceRequirements ?? []),
).rejects.toMatchObject({ code: "service_configuration_missing" })
expect(fetch).not.toHaveBeenCalled()
} finally {
fetch.mockRestore()
}
})
it("refuses Convex business dispatch with the supported-profile reason", async () => {
const hooks = businessServiceComposition(env, [crmModule], "convex")
await expect(
hooks.prepareServices?.(new Container(), context, crmModule.serviceRequirements ?? []),
).rejects.toMatchObject({ code: "service_profile_unsupported" })
})
})
// kit:support:begin
// kit:guides:begin
// Trusted selectors read the same bound ports that perform actual effects.
describe("native help composition", () => {
for (const [host, selectors] of [
["web", webSelectors],
["worker", workerSelectors],
] as const) {
for (const service of ["support", "guides"] as const) {
it(`${host} routes the actual ${service} selector at dispatch and refreshes port changes`, async () => {
let container = new Container()
const descriptor = `sha256:${"a".repeat(64)}`
const source = "b".repeat(40)
const deployment = {
...env.deployment,
CONUSAI_ORG_ID: "1",
CONUSAI_ENVIRONMENT_ID: "3",
CONUSAI_ORG_SLUG: "org",
CONUSAI_WORKSPACE_SLUG: "workspace",
CONUSAI_PROJECT_SLUG: "project",
CONUSAI_PLATFORM_RELEASE_DESCRIPTOR: descriptor,
CONUSAI_PLATFORM_SOURCE_COMMIT: source,
}
const base = crmModule.serviceRequirements[0]
if (!base) throw new Error("CRM fixture requires a service requirement")
const requirement = {
...base,
service,
transport: "platform" as const,
partitionRequired: false,
apiRange: "=1.0.0-preview.20261103",
operations: { "crm.party.list": [service === "support" ? "support:widget-sessions" : "guides:runtime"] },
}
const hooks = businessServiceComposition(
{ deployment },
[{ ...crmModule, serviceRequirements: [requirement] }],
"postgres",
selectors,
)
const readiness = hooks.serviceReadiness
if (!readiness) throw new Error("Native readiness must be composed")
const dispatch = () => readiness.observe(requirement, { ...context, container })
const fetch = vi.spyOn(globalThis, "fetch").mockImplementation(async () =>
Response.json({
component: {
id: service,
observed_at: new Date().toISOString(),
facts: { installed: "true", enabled: "true", entitled: "true", configured: "true", ready: "true" },
},
authorized: "true",
environment_bound: "true",
compatible: "true",
dependencies_ready: "true",
policy_epoch: 2,
descriptor_digest: descriptor,
source_commit: source,
organization_id: 1,
project_id: 9,
environment_id: 3,
app_scope: "org",
authorization_checked_at: new Date().toISOString(),
}),
)
const bind = (selector: string) => {
container = container.child()
if (service === "support")
container.value(kitSupport.SUPPORT_PLATFORM, {
mode: "platform",
channel: selector,
client: {},
} as kitSupport.SupportPlatform)
else
container.value(kitGuides.GUIDES_PLATFORM, {
mode: "platform",
identityNamespace: selector,
client: {},
} as kitGuides.GuidesPlatform)
}
await expect(dispatch()).rejects.toMatchObject({ code: "service_configuration_missing" })
expect(fetch).not.toHaveBeenCalled()
for (const value of ["selected_first", "selected_second"]) {
bind(value)
await dispatch()
const request = JSON.parse(String(fetch.mock.calls.at(-1)?.[1]?.body))
expect(String(fetch.mock.calls.at(-1)?.[0])).toContain(`/platform-services/${service}/readiness`)
expect(request).toMatchObject({
app_scope: "org",
[service === "support" ? "channel" : "identity_namespace"]: value,
})
}
bind("invalid/selector")
await expect(dispatch()).rejects.toMatchObject({ code: "service_configuration_missing" })
expect(fetch).toHaveBeenCalledTimes(2)
container = container.child()
if (service === "support") container.value(kitSupport.SUPPORT_PLATFORM, { mode: "offline" })
else container.value(kitGuides.GUIDES_PLATFORM, { mode: "offline" })
await expect(dispatch()).rejects.toMatchObject({ code: "service_configuration_missing" })
expect(fetch).toHaveBeenCalledTimes(2)
})
}
}
})
it("admits an actual Guides request through trusted composition and fences removed members and revoked native permission", async () => {
const registry = nativeCore.buildRegistry([kitTenancy.tenancyModule, kitGuides.guidesModule])
const data = kitData.createMemoryDataSource({ entities: kitData.entitiesOf(registry, "postgres") })
const root = new Container()
root.value(kitData.DATA_SOURCE, data)
root.value(kitData.SYSTEM_DATA, data)
root.value(kitGuides.GUIDES_REGISTRY, registry)
const descriptor = `sha256:${"a".repeat(64)}`
const source = "b".repeat(40)
const deployment = {
...env.deployment,
CONUSAI_ORG_ID: "1",
CONUSAI_ENVIRONMENT_ID: "3",
CONUSAI_ORG_SLUG: "org",
CONUSAI_WORKSPACE_SLUG: "workspace",
CONUSAI_PROJECT_SLUG: "project",
CONUSAI_PLATFORM_RELEASE_DESCRIPTOR: descriptor,
CONUSAI_PLATFORM_SOURCE_COMMIT: source,
}
let authorized = "true"
const runtimeInputs: unknown[] = []
const fetch = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
if (String(url).includes("/platform-services/guides/readiness"))
return Response.json({
component: {
id: "guides",
observed_at: new Date().toISOString(),
facts: { installed: "true", enabled: "true", entitled: "true", configured: "true", ready: "true" },
},
authorized,
environment_bound: "true",
compatible: "true",
dependencies_ready: "true",
policy_epoch: 2,
descriptor_digest: descriptor,
source_commit: source,
organization_id: 1,
project_id: 9,
environment_id: 3,
app_scope: "org",
authorization_checked_at: new Date().toISOString(),
})
runtimeInputs.push(url instanceof Request ? await url.clone().json() : JSON.parse(String(init?.body)))
return Response.json({ outcomes: [] })
})
root.value(
kitGuides.GUIDES_PLATFORM,
kitGuides.guidesPlatform({
apiUrl: deployment.CONUSAI_API_URL,
token: deployment.CONUSAI_API_TOKEN,
fake: false,
identityNamespace: "selected_namespace",
}),
)
const hooks = businessServiceComposition(
{ deployment },
[kitTenancy.tenancyModule, kitGuides.guidesModule],
"postgres",
webSelectors,
)
const caller = (actor: nativeCore.Actor) => {
const container = root.child()
const tenancy = kitTenancy.createTenancyHooks({ data, registry })
container.value(kitTenancy.TENANCY, tenancy)
return nativeCore.createCaller({ registry, container, tenancy, ...hooks }, { actor })
}
const owner = await data.global(kitTenancy.users).insert({
issuer: "https://identity.example.test",
subject: "owner",
email: "owner@example.test",
displayName: "Owner",
locale: "en",
})
const member = await data.global(kitTenancy.users).insert({
issuer: "https://identity.example.test",
subject: "member",
email: "member@example.test",
displayName: "Member",
locale: "en",
})
const actor = { userId: owner.id }
const alpha = (await caller(actor).callByName("tenancy.org.create", { slug: "alpha", name: "Alpha" })) as {
orgId: string
}
await caller(actor).callByName("tenancy.org.create", { slug: "bravo", name: "Bravo" })
await data.unitOfWork.run({ level: "org", orgId: alpha.orgId, actor }, (tx) =>
tx.of(kitTenancy.orgMemberships).insert({ userId: member.id, role: "viewer" }),
)
const input = {
org: "alpha",
idempotencyKey: "guides:test",
events: [
{
schema_version: 1,
event_id: "018f0000-0000-7000-8000-000000000001",
occurred_at: new Date().toISOString(),
type: "track",
name: "test",
},
],
}
const dispatch = (org = "alpha") =>
caller({ userId: member.id }).callByName("guides.runtime.record", { ...input, org })
await expect(dispatch("bravo")).rejects.toMatchObject({ status: 404 })
expect(fetch).not.toHaveBeenCalled()
await dispatch()
expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toMatchObject({
app_scope: "org",
identity_namespace: "selected_namespace",
})
expect(fetch).toHaveBeenCalledTimes(2)
expect(runtimeInputs[0]).toMatchObject({
identity: { identity_namespace: "selected_namespace", subject: member.id, app_tenant: alpha.orgId },
})
authorized = "false"
await expect(dispatch()).rejects.toMatchObject({ code: "service_permission_missing" })
expect(fetch).toHaveBeenCalledTimes(3)
await caller(actor).callByName("tenancy.member.remove", { org: "alpha", userId: member.id })
authorized = "true"
await expect(dispatch()).rejects.toMatchObject({ status: 404 })
expect(fetch).toHaveBeenCalledTimes(3)
})
// kit:guides:end
// kit:support:end
+122
View File
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A `transport: "business"` procedure on the fake platform of @conusai-kit/testing, composed as the
// app composes it (`businessServiceComposition`): binding lookup and partition admission over HTTP,
// then the CRM service in the binding's partition. A fake platform; no live readiness is asserted.
import { type Actor, buildRegistry, Container, createCaller } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
import { fakeNativeServices, startFakePlatform } from "@conusai-kit/testing/fake-platform"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import { bindOnFakePlatform, declaredBusinessScopes } from "../apps/worker/src/fake-business-bindings.js"
import { businessServiceComposition } from "../config/src/business-services.js"
import { crmModule } from "../modules/crm/module.js"
const simulated = fakeNativeServices({ token: "dev-token" })
let platform: Awaited<ReturnType<typeof startFakePlatform>>
beforeAll(async () => {
platform = await startFakePlatform(0, { deployment: simulated.authority })
})
afterAll(async () => {
await platform.close()
})
async function app() {
const registry = buildRegistry([tenancyModule, crmModule])
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
const env = {
platform: "fake" as const,
production: false,
deployment: { ...simulated.deployment, CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "dev-token" },
}
const hooks = businessServiceComposition(env, [crmModule], "postgres")
const caller = (actor: Actor) => {
const container = root.child()
const tenancy = createTenancyHooks({ data, registry })
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, ...hooks }, { actor })
}
const owner = await data.global(users).insert({
issuer: "https://identity.example.test",
subject: "owner",
email: "owner@example.test",
displayName: "Owner",
locale: "en",
})
const call = caller({ userId: owner.id })
const org = (await call.callByName("tenancy.org.create", { slug: "buyer", name: "Buyer" })) as { orgId: string }
await call.callByName("tenancy.workspace.create", {
org: "buyer",
slug: "purchasing",
name: "Purchasing",
visibility: "org",
})
await call.callByName("tenancy.project.create", {
org: "buyer",
workspace: "purchasing",
slug: "suppliers",
name: "Suppliers",
moduleKey: "crm",
})
return {
env,
registry,
call,
orgId: org.orgId,
project: { org: "buyer", workspace: "purchasing", project: "suppliers" },
}
}
describe("a business procedure on the fake platform", () => {
it("binds what the selected modules declare, and nothing for an app without business modules", () => {
expect(declaredBusinessScopes(buildRegistry([tenancyModule, crmModule]))).toEqual({
crm: expect.arrayContaining(["crm.parties:read", "crm.parties:write"]),
})
expect(declaredBusinessScopes(buildRegistry([tenancyModule]))).toEqual({})
})
it("is refused until the app tenant is bound, then admitted and served from its partition", async () => {
const { env, registry, call, orgId, project } = await app()
await expect(call.callByName("crm.party.list", project)).rejects.toMatchObject({
code: "service_partition_missing",
})
expect(await bindOnFakePlatform(env, registry, [orgId])).toBe(1)
const created = (await call.callByName("crm.party.create", {
...project,
kind: "organization",
displayName: "Synthetic supplier",
country: "LT",
requestId: "01900000-0000-7000-8000-000000000010",
})) as { partyId: string; displayName: string }
expect(created.displayName).toBe("Synthetic supplier")
const listed = (await call.callByName("crm.party.list", project)) as { rows: Array<{ partyId: string }> }
expect(listed.rows.map((row) => row.partyId)).toEqual([created.partyId])
const binding = platform.business?.calls.find((entry) => entry.binding !== undefined)?.binding
expect(binding).toBeDefined()
expect(platform.business?.partition(binding as string).crm.parties.size).toBe(1)
})
it("is refused by name once the organization disables the service", async () => {
const { env, registry, call, orgId, project } = await app()
await bindOnFakePlatform(env, registry, [orgId])
platform.business?.disable("crm")
try {
await expect(call.callByName("crm.party.list", project)).rejects.toMatchObject({ code: "service_not_enabled" })
} finally {
platform.business?.enable("crm")
}
})
it("never provisions outside a development run on the fake platform", async () => {
const { env, registry, orgId } = await app()
await expect(bindOnFakePlatform({ ...env, platform: "platform" }, registry, [orgId])).rejects.toThrow(
/administrator/,
)
await expect(bindOnFakePlatform({ ...env, production: true }, registry, [orgId])).rejects.toThrow(/administrator/)
})
})
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The sign-out control ends every enabled module's browser context (joint contract §12, J1). Sign-out
// is a full navigation, so no provider unmounts: without the lifecycle run, the guides progress and
// unsent events of the person who signed out stay in the tab's `sessionStorage`.
import { afterEach, describe, expect, it, vi } from "vitest"
import { composedClientModules, endClientModules } from "../apps/web/src/ui/client-module-list"
/** A `Storage` over a Map, as much of it as the modules' handlers use. */
function memoryStorage(seed: Record<string, string>): Storage {
const values = new Map(Object.entries(seed))
const storage = {
get length() {
return values.size
},
key: (index: number) => [...values.keys()][index] ?? null,
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => values.clear(),
}
// `Object.keys(storage)` lists the stored keys, as it does for the browser's own.
return new Proxy(storage, {
ownKeys: () => [...values.keys()],
getOwnPropertyDescriptor: (_target, key) =>
typeof key === "string" && values.has(key)
? { enumerable: true, configurable: true, value: values.get(key) }
: undefined,
}) as unknown as Storage
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe("signing out ends the client modules' context", () => {
it("clears what each enabled module keeps in the tab and leaves the app's own keys", async () => {
const own = "app.preference"
const keys = [own]
keys.push("conusai.guides.acme.u-7.queue", "conusai.guides.beta.u-7.active") // kit:guides
keys.push("conusai.support.session.v1:spk_example") // kit:support
const storage = memoryStorage(Object.fromEntries(keys.map((key) => [key, "{}"])))
vi.stubGlobal("window", { sessionStorage: storage })
const report = await endClientModules("sign-out")
expect(report.ok).toBe(true)
expect(report.outcomes.map((outcome) => outcome.module).sort()).toEqual([...composedClientModules.modules].sort())
expect(Object.keys(storage)).toEqual([own])
})
it("issues a new context instance, so a late answer of the old one is discarded", async () => {
vi.stubGlobal("window", { sessionStorage: memoryStorage({}) })
const before = composedClientModules.current()
await endClientModules("sign-out")
expect(before.signal.aborted).toBe(true)
expect(composedClientModules.isCurrent(before.instance)).toBe(false)
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["guides", "support"]),
}))
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Actual module UI and HTTP data client; synthetic server responses only.
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
import { createDataClient, DataProvider } from "@conusai-kit/ui/data"
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
import { createRoot } from "react-dom/client"
import { en } from "../../modules/knowledge/i18n/en.js"
import { searchFormLabels } from "../../modules/knowledge/ui/labels.js"
import { SearchForm } from "../../modules/knowledge/ui/search-form.js"
import "@conusai-kit/ui/styles.css"
import "@conusai-kit/ui/primitives.css"
import "@conusai-kit/ui/patterns.css"
const scenario = new URLSearchParams(location.search).get("scenario") ?? "cited"
const client = createDataClient({ headers: () => ({ "x-fixture-scenario": scenario }) })
const t = (key: string, values: Record<string, string | number> = {}): string => {
let value: unknown = en
for (const part of key.split(".")) value = (value as Record<string, unknown>)[part]
if (typeof value !== "string") throw new Error("missing fixture message")
for (const [key, replacement] of Object.entries(values)) value = value.replace(`{${key}}`, String(replacement))
return value
}
const root = document.getElementById("root")
if (!root) throw new Error("missing fixture root")
createRoot(root).render(
<UiLabelsProvider labels={uiLabels(createI18n("en", builtInCatalogues))}>
<DataProvider client={client}>
<SearchForm
slugs={{ org: "synthetic", workspace: "workspace", project: "project" }}
collectionName="sources"
labels={searchFormLabels(t)}
locale="en"
/>
</DataProvider>
</UiLabelsProvider>,
)
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env node
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Chromium + actual module/client, local synthetic HTTP. No provider/auth proof.
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import { createHash } from "node:crypto"
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"
import http from "node:http"
import os from "node:os"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { chromium } from "@playwright/test"
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
const evidence = process.env.LIFECYCLE_BROWSER_EVIDENCE
if (!evidence) throw new Error("LIFECYCLE_BROWSER_EVIDENCE must name a new external directory")
await mkdir(evidence, { mode: 0o700 })
const temp = await mkdtemp(path.join(os.tmpdir(), "conusai-knowledge-browser-"))
const build = spawnSync(
"bun",
[
"build",
"test/component-browser/knowledge.fixture.tsx",
"--target",
"browser",
"--format",
"iife",
"--outdir",
temp,
"--entry-naming",
"fixture.[ext]",
],
{ cwd: root, encoding: "utf8" },
)
if (build.status !== 0) throw new Error(`fixture build failed: ${build.stdout}${build.stderr}`)
const assets = new Set(await readdir(temp))
const network = []
const errors = []
const consoleErrors = []
const server = http.createServer(async (req, res) => {
if (req.url === "/api/v1/knowledge.search") {
const scenario = req.headers["x-fixture-scenario"]
const status = scenario === "configuration" ? 503 : 200
const body =
status === 503
? { status: 503, code: "configuration_missing", title: "Configuration required" }
: {
results:
scenario === "revoked"
? []
: [
{
fileId: "synthetic-file",
filename: "synthetic-source.txt",
collectionName: "sources",
snippet: "A synthetic passage with a current source citation.",
score: 0.9,
chunk: 1,
page: 3,
blockId: "p3-b2",
chunkId: "22222222-2222-4222-8222-222222222222",
},
],
}
network.push({ method: req.method, path: req.url, scenario, status })
res.writeHead(status, { "content-type": "application/json" })
res.end(JSON.stringify(body))
return
}
if (req.url === "/favicon.ico") {
res.writeHead(204)
res.end()
return
}
if (req.url?.startsWith("/") && assets.has(req.url.slice(1))) {
res.writeHead(200, {
"content-type": req.url.endsWith(".css")
? "text/css"
: req.url.endsWith(".woff2")
? "font/woff2"
: "application/javascript",
})
res.end(await readFile(path.join(temp, req.url.slice(1))))
return
}
res.writeHead(200, { "content-type": "text/html" })
res.end(
'<!doctype html><html lang="en"><head><title>Knowledge lifecycle fixture</title><link rel="stylesheet" href="/fixture.css"></head><body><main id="root"></main><script src="/fixture.js"></script></body></html>',
)
})
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
const browser = await chromium.launch({ headless: true })
try {
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } })
page.on("pageerror", (error) => errors.push(error.message))
page.on("console", (message) => {
if (message.type() === "error") {
consoleErrors.push(message.text())
if (!/503/.test(message.text())) errors.push(message.text())
}
})
const base = `http://127.0.0.1:${server.address().port}`
for (const scenario of ["cited", "revoked", "configuration"]) {
await page.goto(`${base}/?scenario=${scenario}`)
await page.getByRole("searchbox", { name: "Question" }).fill("synthetic source")
await page.getByRole("button", { name: "Search", exact: true }).click()
if (scenario === "cited") {
await page.getByText("A synthetic passage with a current source citation.", { exact: true }).waitFor()
assert.match(await page.locator("ol").innerText(), /synthetic-source.txt.*result 1.*page 3/s)
await page.getByText("p3-b2", { exact: true }).waitFor()
} else if (scenario === "revoked") {
await page.getByText("No answers in this collection.", { exact: true }).waitFor()
assert.equal(await page.locator("blockquote").count(), 0)
} else {
await page.getByRole("alert").filter({ hasText: "Set CONUSAI_ENVIRONMENT" }).waitFor()
assert.equal(await page.locator("blockquote").count(), 0)
}
await page.screenshot({ path: path.join(evidence, `${scenario}.png`), fullPage: true })
}
assert.deepEqual(errors, [])
const sources = {}
for (const file of [
"modules/knowledge/ui/search-form.tsx",
"modules/knowledge/ui/labels.ts",
"modules/knowledge/contract/procedures.ts",
"test/component-browser/knowledge.fixture.tsx",
])
sources[file] = createHash("sha256")
.update(await readFile(path.join(root, file)))
.digest("hex")
await writeFile(
path.join(evidence, "result.json"),
JSON.stringify(
{
tier: "component-browser-synthetic-http",
sourceCommit:
process.env.LIFECYCLE_SOURCE_COMMIT ??
spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).stdout.trim(),
sources,
network,
consoleErrors,
unexpectedErrors: errors,
result: "passed",
},
null,
2,
),
)
console.log("Knowledge citations, revocation and configuration refusals passed; fixture browser only")
} finally {
await browser.close()
await new Promise((resolve) => server.close(resolve))
await rm(temp, { recursive: true, force: true })
}
+298
View File
@@ -0,0 +1,298 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: a ${NAME} in this file is what the
// compose document holds verbatim -- compose interpolates it from the deployment's env file -- so
// the assertions compare against that text and not against a value.
// compose.yaml: the ordering the platform does not give (decision AT-D14), and the keys the
// platform's own compose model accepts.
//
// The model is crates/conusai-deployer/src/compose_model.rs: a service key it has no field for
// fails the deployment's parse, and so does a host binding in `ports`. This suite is that list,
// read from the document, so a key added here in a hurry fails in review and not on stage.
import { readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { parse } from "yaml"
interface Service {
build?: { context: string; dockerfile: string; target?: string }
image?: string
command?: string[]
environment?: Record<string, string>
expose?: (string | number)[]
ports?: unknown[]
depends_on?: Record<string, { condition: string }>
restart?: string
healthcheck?: { test: string[]; interval?: string }
deploy?: { resources?: { limits?: { cpus?: string; memory?: string } } }
init?: boolean
working_dir?: string
stop_grace_period?: string
read_only?: boolean
tmpfs?: string[]
}
const document = parse(readFileSync(new URL("../compose.yaml", import.meta.url), "utf8")) as {
services: Record<string, Service>
}
const services = document.services
const optional = (name: string): string => `\${${name}:-}`
/**
* The passthroughs every profile owes each process (BUG-APP-02): a variable the code reads but
* compose never names never reaches the container, so the deployment's value silently loses to
* the default. `${VAR}` where boot refuses to run without it, `${VAR:-}` where a default stands.
*/
function expectPassthroughs(named: Record<string, Service>): void {
for (const name of [
"OIDC_WEB_CLIENT_SECRET",
"OIDC_RESOURCE",
"OIDC_INTROSPECTION_CLIENT_ID",
"OIDC_INTROSPECTION_CLIENT_SECRET",
"OIDC_RESOURCE_STATUS_URL",
"OIDC_BACKCHANNEL_URL",
"OIDC_SCOPES",
// The identity link's admission credential and id (ADR-062): an invitation admits its address.
"OIDC_LINK_ID",
"OIDC_ADMISSION_TOKEN",
"APP_ORGANIZATION_SELF_SERVICE",
"AUTH_PROVIDER_GENERATION",
"AUTH_COOKIE_NAME",
"AUTH_SESSION_IDLE_SECONDS",
"AUTH_SESSION_ABSOLUTE_SECONDS",
]) {
expect(named.web?.environment?.[name], `web ${name}`).toBe(optional(name))
}
for (const name of ["web", "worker"] as const) {
if (named[name]) expect(named[name]?.environment?.CONUSAI_ORG_SLUG).toBe(optional("CONUSAI_ORG_SLUG"))
}
// The knowledge client's routes name the workspace and the project (K0): compose passes only what it lists.
for (const name of ["web", "worker"] as const) {
for (const variable of ["CONUSAI_WORKSPACE_SLUG", "CONUSAI_PROJECT_SLUG"]) {
if (named[name]) expect(named[name]?.environment?.[variable], `${name} ${variable}`).toBe(optional(variable))
}
}
if (named.worker) {
expect(named.worker.environment?.CONUSAI_PUBLIC_URL).toBe("${CONUSAI_PUBLIC_URL}")
expect(named.worker.environment?.DOCUMENTS_EXCLUDED_PAGE_CLASSES).toBe(optional("DOCUMENTS_EXCLUDED_PAGE_CLASSES"))
}
if (named.migrate) expect(named.migrate.environment?.CONUSAI_RELEASE).toBe("${CONUSAI_RELEASE}")
for (const name of [
"CONUSAI_ENVIRONMENT",
"CONUSAI_DSN",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
]) {
if (named.migrate) expect(named.migrate.environment?.[name], `migrate ${name}`).toBe(optional(name))
}
}
/** Every key TenantService models (compose_model.rs). Anything else fails the platform's parse. */
const MODELLED = new Set([
"image",
"build",
"command",
"environment",
"ports",
"expose",
"volumes",
"depends_on",
"restart",
"healthcheck",
"deploy",
"init",
"stop_grace_period",
"stop_signal",
"working_dir",
"user",
"read_only",
"tmpfs",
])
describe("compose.yaml", () => {
it("uses the platform's exact read-only private temporary filesystem profile", () => {
expect(services.web?.read_only).toBe(true)
expect(services.web?.tmpfs).toEqual(["/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777"])
})
it("has the three services of conusai.json and nothing else", () => {
expect(Object.keys(services).sort()).toEqual(["migrate", "web", "worker"])
})
it("uses only the keys the platform's compose model has a field for", () => {
for (const [name, service] of Object.entries(services)) {
for (const key of Object.keys(service)) {
expect(MODELLED.has(key), `${name}.${key} is not in the platform's compose model`).toBe(true)
}
}
})
it("builds every service from one Dockerfile, so one image serves all three", () => {
const builds = Object.values(services).map((service) => JSON.stringify(service.build))
expect(new Set(builds).size).toBe(1)
expect(Object.values(services).every((service) => service.image === undefined)).toBe(true)
})
it("starts each process with its own argv, never a shell string", () => {
for (const [name, service] of Object.entries(services)) {
expect(Array.isArray(service.command), `${name} has no argv command`).toBe(true)
}
expect(services.migrate?.command?.at(-1)).toContain("migrate.js")
})
it("runs the migrate job to completion before web and worker start", () => {
// `kind: job` in conusai.json is inert at deploy time: one `docker compose up -d --wait`
// starts everything at once, and `pre_deploy_command` has no executor at all. This is the
// ordering (decision AT-D14).
expect(services.migrate?.restart).toBe("no")
for (const name of ["web", "worker"] as const) {
expect(services[name]?.depends_on?.migrate).toEqual({ condition: "service_completed_successfully" })
}
})
it("publishes no host port", () => {
for (const service of Object.values(services)) expect(service.ports).toBeUndefined()
expect(services.web?.expose).toEqual(["3000"])
})
it("gives every service a limit, and the two long-running ones a healthcheck", () => {
for (const [name, service] of Object.entries(services)) {
expect(service.deploy?.resources?.limits?.memory, `${name} has no memory limit`).toBeTruthy()
expect(service.deploy?.resources?.limits?.cpus, `${name} has no cpu limit`).toBeTruthy()
expect(service.init, `${name} does not reap its children`).toBe(true)
}
expect(services.web?.healthcheck?.test).toBeTruthy()
expect(services.worker?.healthcheck?.test).toBeTruthy()
// A job is not healthy or unhealthy; it succeeds or it fails.
expect(services.migrate?.healthcheck).toBeUndefined()
})
it("hands the owner's URL to the migrate job alone", () => {
expect(services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBe("${POSTGRES_MIGRATE_URL}")
expect(services.web?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
expect(services.worker?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
for (const name of ["web", "worker", "migrate"] as const) {
expect(services[name]?.environment?.POSTGRES_URL).toBe("${POSTGRES_URL}")
expect(services[name]?.environment?.POSTGRES_READER_URL).toBe("${POSTGRES_READER_URL}")
}
})
it("passes every variable each process reads through to its container", () => {
expectPassthroughs(services)
})
})
// kit:profile-dialect:begin
describe("profiles/mariadb/compose.yaml", () => {
const profile = parse(readFileSync(new URL("../profiles/mariadb/compose.yaml", import.meta.url), "utf8")) as {
services: Record<string, Service>
}
it("hands the one MariaDB login to every process and the Valkey queue to web and worker", () => {
for (const name of ["web", "worker", "migrate"] as const) {
expect(profile.services[name]?.environment?.MARIADB_URL).toBe("${MARIADB_URL}")
}
for (const name of ["web", "worker"] as const) {
expect(profile.services[name]?.environment?.REDIS_URL).toBe("${REDIS_URL}")
}
// MariaDB has no row-level security: there is no owner/runtime/reader split to hand out.
expect(profile.services.migrate?.environment?.POSTGRES_MIGRATE_URL).toBeUndefined()
})
it("passes every variable each process reads through to its container", () => {
expectPassthroughs(profile.services)
})
})
for (const name of ["mongodb", "convex"] as const) {
describe(`profiles/${name}/compose.yaml`, () => {
const profile = parse(readFileSync(new URL(`../profiles/${name}/compose.yaml`, import.meta.url), "utf8")) as {
services: Record<string, Service>
}
it("passes the platform billing slug and web auth settings through to the containers", () => {
for (const service of ["web", "worker"] as const) {
if (profile.services[service]) {
expect(profile.services[service]?.environment?.CONUSAI_ORG_SLUG).toBe(
name === "convex" ? `\${CONUSAI_ORG_SLUG}` : optional("CONUSAI_ORG_SLUG"),
)
for (const variable of ["CONUSAI_WORKSPACE_SLUG", "CONUSAI_PROJECT_SLUG"]) {
expect(profile.services[service]?.environment?.[variable], `${service} ${variable}`).toBe(
optional(variable),
)
}
}
}
for (const variable of [
"OIDC_WEB_CLIENT_SECRET",
"OIDC_RESOURCE",
"OIDC_INTROSPECTION_CLIENT_ID",
"OIDC_INTROSPECTION_CLIENT_SECRET",
"OIDC_RESOURCE_STATUS_URL",
"OIDC_BACKCHANNEL_URL",
"OIDC_SCOPES",
"OIDC_LINK_ID",
"OIDC_ADMISSION_TOKEN",
"APP_ORGANIZATION_SELF_SERVICE",
"AUTH_PROVIDER_GENERATION",
"AUTH_COOKIE_NAME",
"AUTH_SESSION_IDLE_SECONDS",
"AUTH_SESSION_ABSOLUTE_SECONDS",
]) {
expect(profile.services.web?.environment?.[variable], `web ${variable}`).toBe(optional(variable))
}
})
})
}
// kit:profile-dialect:end
describe("the local stack", () => {
const dev = readFileSync(new URL("../compose.dev.yaml", import.meta.url), "utf8")
const roles = readFileSync(new URL("../dev/postgres-roles.sql", import.meta.url), "utf8")
const example = readFileSync(new URL("../.env.example", import.meta.url), "utf8")
it("installs the roles script through dev/postgres-entrypoint.sh, whatever mode the checkout gave it", () => {
// A clone under `umask 077` writes the script 0600, which the server's user cannot read under rootless
// Docker: no stack mounts it where the server reads it (template lesson core-15).
const stacks = [
"compose.dev.yaml", // `pnpm dev`
"e2e/support/stack.ts", // `pnpm e2e`
"e2e/better-auth/stack.ts", // kit:convex-better-auth
"test/relay-scope.test.ts", // the worker's own relay
]
for (const stack of stacks) {
const text = readFileSync(new URL(`../${stack}`, import.meta.url), "utf8")
expect(text, stack).not.toContain("docker-entrypoint-initdb.d")
expect(text, stack).toContain("/conusai-dev/postgres-entrypoint.sh")
}
})
it("creates the three roles the platform creates, with the same powers", () => {
for (const role of ["appdev_owner", "appdev_app", "appdev_reader"]) {
expect(roles, `${role} is not created`).toMatch(new RegExp(`CREATE ROLE ${role}\\s+LOGIN`))
}
// The three are LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION,
// as data-services DS21 creates them.
expect(roles.match(/NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT NOREPLICATION/g)).toHaveLength(3)
// The owner owns schema public; the runtime role gets rows, never ownership.
expect(roles).toContain('ALTER SCHEMA public OWNER TO "appdev_owner"')
expect(roles).toContain('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "appdev_app"')
// The reader gets no table privilege here: the app's own migrations grant it what it reads.
expect(roles).not.toMatch(/GRANT SELECT ON (ALL )?TABLES? [^;]*"appdev_reader"/)
})
it("hands each local process the same URL the platform would", () => {
expect(example).toContain("POSTGRES_URL=postgresql://appdev_app:")
expect(example).toContain("POSTGRES_READER_URL=postgresql://appdev_reader:")
expect(example).toContain("POSTGRES_MIGRATE_URL=postgresql://appdev_owner:")
})
it("keeps the development stack out of what is deployed", () => {
// conusai.json names compose.yaml alone, so nothing in compose.dev.yaml is ever deployed.
expect(dev).toContain("image: postgres:18")
expect(readFileSync(new URL("../conusai.json", import.meta.url), "utf8")).not.toContain("compose.dev.yaml")
})
})
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { createTestObserve } from "@conusai-sdk/observe-testing"
import { describe, expect, it } from "vitest"
import { browserObserveOptions, observeMeta, startBrowserObserve } from "../apps/web/src/observe/browser.js"
import {
CONSENT_COOKIE,
CONSENT_NOTICE_VERSION,
CONSENT_SECONDS,
consentCookie,
privacyRestricted,
readConsentChoice,
recordConsentChoice,
} from "../apps/web/src/observe/consent.js"
const NOW = Date.parse("2026-10-01T12:00:00.000Z")
const choice = (analytics: boolean, replay: boolean) => ({
analytics,
replay,
at: new Date(NOW).toISOString(),
notice: CONSENT_NOTICE_VERSION,
})
describe("the app's consent record", () => {
it("stores both independent choices for six months and rejects malformed or stale records", () => {
const saved = choice(true, false)
const cookie = consentCookie(saved)
expect(cookie).toContain(`${CONSENT_COOKIE}=`)
expect(cookie).toContain(`Max-Age=${CONSENT_SECONDS}`)
expect(cookie).toContain("SameSite=Lax; Secure")
expect(readConsentChoice(cookie, NOW)).toEqual(saved)
expect(readConsentChoice(cookie, NOW + CONSENT_SECONDS * 1000)).toBeUndefined()
expect(readConsentChoice(`${CONSENT_COOKIE}=not-json`, NOW)).toBeUndefined()
expect(readConsentChoice(consentCookie({ ...saved, notice: "old" }), NOW)).toBeUndefined()
})
it("does not infer replay from analytics consent", () => {
const settings = { release: "r", environment: "production" }
expect(browserObserveOptions(settings, choice(true, false)).modules).toEqual({ traces: true, analytics: true })
expect(browserObserveOptions(settings, choice(true, true)).modules).toEqual({
traces: true,
analytics: true,
replay: true,
})
expect(browserObserveOptions(settings, choice(true, true), true).modules).not.toHaveProperty("replay")
})
})
describe("restoring and changing a choice", () => {
const documentWith = (cookie: string) => ({
cookie,
querySelector: (selector: string) =>
selector === 'meta[name="app-observe"]'
? { getAttribute: (name: string) => observeMeta({ release: "r", environment: "e" })[name] ?? null }
: null,
})
it("restores analytics without granting replay, and revokes everything on withdrawal", () => {
const instance = createTestObserve({ consent: "opt-in", modules: { errors: false } })
startBrowserObserve(documentWith(consentCookie(choice(true, false))) as never, () => instance)
expect(instance.consent.state()).toBe("granted")
recordConsentChoice({ analytics: false, replay: false })
expect(instance.consent.state()).toBe("denied")
})
it("treats GPC as a refusal even when an older cookie granted both purposes", () => {
const instance = createTestObserve({ consent: "opt-in", modules: { errors: false } })
startBrowserObserve(documentWith(consentCookie(choice(true, true))) as never, () => instance, {
globalPrivacyControl: true,
})
expect(instance.consent.state()).toBe("denied")
expect(privacyRestricted({ doNotTrack: "1" })).toBe(true)
expect(privacyRestricted({ globalPrivacyControl: false })).toBe(false)
})
})
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A deployment the contract refuses says why: `/api/ready` names what to set, every other route is a 503 and no route
// a 500, and a message that quotes a value stays in the log. And signing out can reach the identity provider.
import { describe, expect, it } from "vitest"
import { issuerOrigins, refusalOf, refusalResponse } from "../apps/web/src/server/contract"
const refusal = refusalOf(new Error("OIDC_ISSUER is required in production"))
const request = (path: string): Request => new Request(`https://app.example.test${path}`)
describe("a process the deployment contract refused", () => {
it("names what to set on /api/ready", async () => {
const response = refusalResponse(request("/api/ready"), refusal)
expect(response.status).toBe(503)
expect(await response.json()).toEqual({
ready: false,
checks: { contract: false },
reason: "OIDC_ISSUER is required in production",
})
})
it("answers every other route 503, and says nothing more there", async () => {
const response = refusalResponse(request("/acme"), refusal)
expect(response.status).toBe(503)
expect(response.headers.get("retry-after")).toBe("30")
expect(await response.json()).toEqual({ status: 503, code: "not_configured" })
})
it("keeps a value out of what the public can read", async () => {
const quoted = refusalOf(new Error('CONUSAI_API_URL is "https://secret.example.test"'))
expect(quoted.message).toContain("secret.example.test")
expect(JSON.stringify(await refusalResponse(request("/api/ready"), quoted).json())).not.toContain("secret")
})
})
describe("the form-action of the content security policy", () => {
it("adds the issuer's origin, without its path", () => {
expect(issuerOrigins("https://id.example.test/oauth/v2")).toEqual(["https://id.example.test"])
})
it("adds nothing with no issuer", () => {
expect(issuerOrigins(undefined)).toEqual([])
expect(issuerOrigins(" ")).toEqual([])
})
})
+110
View File
@@ -0,0 +1,110 @@
// 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()
}
})
})
+74
View File
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import {
applyIdentityMappings,
IDENTITY_MIGRATION_FORMAT,
type IdentityMigrationBundle,
identityIssuerCoverage,
identityMigrationDigest,
} from "@conusai-kit/tenancy"
import { describe, expect, it, vi } from "vitest"
import { createConvexAuthDataSource } from "../apps/web/src/server/convex-auth-data"
const calls = vi.hoisted(() => ({
queries: [] as { args: unknown }[],
mutations: [] as { args: unknown }[],
}))
vi.mock("convex/browser", () => ({
ConvexHttpClient: class {
setAdminAuth(): void {}
query(_reference: unknown, args: unknown): Promise<unknown> {
calls.queries.push({ args })
return Promise.resolve({ source: 1, target: 1 })
}
mutation(_reference: unknown, args: unknown): Promise<unknown> {
calls.mutations.push({ args })
return Promise.resolve({ migrationId: "7", applied: 1 })
}
},
}))
// The template ships without the platform repository, so the bundle is built here with the kit's own
// digest; the kit's tests pin that digest to the platform's shared fixture.
const unsigned = {
migration_id: 7,
source: { issuer: "https://old.example.test", audience: "old-audience" },
target: { issuer: "https://new.example.test/api/auth", audience: "urn:conusai:identity:2:3:4" },
mappings: [{ source_subject: "source-1", target_subject: "target-1" }],
}
const sharedBundle: IdentityMigrationBundle = {
format: IDENTITY_MIGRATION_FORMAT,
...unsigned,
digest: await identityMigrationDigest(unsigned),
}
describe("Convex auth identity migrations", () => {
it("uses the private mapping mutation and coverage query", async () => {
calls.queries.length = 0
calls.mutations.length = 0
const data = createConvexAuthDataSource({ url: "https://convex.example.test", adminKey: "test-admin-key" })
const bundle = sharedBundle
const options = {
bundle,
staged: {
migrationId: String(bundle.migration_id),
digest: bundle.digest,
targetIssuer: bundle.target.issuer,
targetAudience: bundle.target.audience,
},
active: bundle.source,
}
await applyIdentityMappings(data, options)
expect(calls.mutations).toEqual([{ args: { options } }])
expect(await identityIssuerCoverage(data, { source: bundle.source.issuer, target: bundle.target.issuer })).toEqual({
source: 1,
target: 1,
})
expect(calls.queries).toEqual([
{ args: { issuers: { source: bundle.source.issuer, target: bundle.target.issuer } } },
])
})
})
+217
View File
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
import type { AddressInfo } from "node:net"
import { afterEach, describe, expect, it, vi } from "vitest"
import { syncPlatformEnvironment } from "../profiles/convex/sync-env.mjs"
const servers: Server[] = []
afterEach(async () => {
await Promise.all(
servers.splice(0).map(
(server) =>
new Promise<void>((resolve) => {
server.closeAllConnections()
server.close(() => resolve())
}),
),
)
})
/** A promise the test resolves when something it waits on has happened (the tests' lib is ES2023). */
function signal() {
let resolve = () => {}
const promise = new Promise<void>((done) => {
resolve = done
})
return { promise, resolve }
}
async function listen(handler: (req: IncomingMessage, res: ServerResponse) => void) {
const server = createServer(handler)
servers.push(server)
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
return `http://127.0.0.1:${(server.address() as AddressInfo).port}`
}
async function fixture(options: { queryStatus?: number; updateStatus?: number } = {}) {
const values = new Map([
["EXISTING", "fixture-original"],
["UNLISTED", "fixture-untouched"],
])
const requests: string[] = []
let authenticated = true
const url = await listen((req, res) => {
requests.push(req.url ?? "")
authenticated &&= req.headers.authorization === "Convex fixture-placeholder"
const chunks: Buffer[] = []
req.on("data", (chunk) => chunks.push(chunk))
req.on("end", () => {
if (req.url === "/api/query") {
const body = JSON.parse(Buffer.concat(chunks).toString())
if (body.path !== "_system/cli/queryEnvironmentVariables" || req.method !== "POST") {
res.writeHead(400).end()
return
}
res.writeHead(options.queryStatus ?? 200, { "Content-Type": "application/json" })
res.end(
JSON.stringify({
status: "success",
value: [...values].map(([name, value]) => ({ name, value })),
}),
)
} else if (req.url === "/api/update_environment_variables") {
if (options.updateStatus) {
res.writeHead(options.updateStatus).end("fixture-sensitive-error-body")
return
}
const body = JSON.parse(Buffer.concat(chunks).toString())
for (const entry of body.changes) values.set(entry.name, entry.value)
res.writeHead(200).end()
} else {
res.writeHead(404).end()
}
})
})
// The sync's own default deadline (30 s) is only a hang bound here: these cases end on the
// fixture's answers, and the deadline itself is tested with a faked clock below.
const sync = (variables: Record<string, string>) =>
syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables })
return { values, requests, sync, authenticated: () => authenticated }
}
describe("Convex deployment environment sync through HTTP-only relays", () => {
it("creates missing values, skips equal values and preserves unlisted values", async () => {
const backend = await fixture()
await backend.sync({ NEW: "fixture-new", EXISTING: "fixture-original" })
expect(backend.values.get("NEW")).toBe("fixture-new")
expect(backend.values.get("EXISTING")).toBe("fixture-original")
expect(backend.values.get("UNLISTED")).toBe("fixture-untouched")
expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables"])
expect(backend.authenticated()).toBe(true)
await backend.sync({ NEW: "fixture-new", EXISTING: "fixture-original" })
expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables", "/api/query"])
})
it("refuses the entire batch on a conflict, without overwriting or adding values", async () => {
const backend = await fixture()
await expect(backend.sync({ NEW: "fixture-new", EXISTING: "fixture-conflict" })).rejects.toThrow(
"Convex platform service environment sync failed",
)
expect(backend.values.has("NEW")).toBe(false)
expect(backend.values.get("EXISTING")).toBe("fixture-original")
expect(backend.requests).toEqual(["/api/query"])
})
it.each([401, 403, 500])("fails a refused query (%i) without update or automatic retries", async (queryStatus) => {
const backend = await fixture({ queryStatus })
await expect(backend.sync({ NEW: "fixture-new" })).rejects.toThrow(
"Convex platform service environment sync failed",
)
expect(backend.requests).toEqual(["/api/query"])
expect(backend.values.has("NEW")).toBe(false)
})
it("fails an update without exposing the response body or retrying it", async () => {
const backend = await fixture({ updateStatus: 503 })
await expect(backend.sync({ NEW: "fixture-new" })).rejects.toThrow(
/^Convex platform service environment sync failed$/,
)
expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables"])
expect(backend.values.has("NEW")).toBe(false)
})
it("bounds the whole query and update sequence with one deadline", async () => {
// The deadline is the sync's own timer, so the test owns the clock: only setTimeout and
// clearTimeout are faked, and each response is held until the test has moved time. Nothing
// here depends on how fast the host runs; a former real-time version (80 ms responses, a 120 ms
// deadline) failed whenever the query alone took longer than the deadline.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] })
try {
const requests: string[] = []
const arrived = { query: signal(), update: signal() }
let releaseQuery = () => {}
const url = await listen((req, res) => {
requests.push(req.url ?? "")
req.resume()
req.on("end", () => {
if (req.url === "/api/query") {
releaseQuery = () => {
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ status: "success", value: [] }))
}
arrived.query.resolve()
} else {
// Held: only the deadline can end this request.
arrived.update.resolve()
}
})
})
const sync = syncPlatformEnvironment({
url,
adminKey: "fixture-placeholder",
variables: { NEW: "fixture-new" },
timeoutMs: 1000,
})
let settled = false
sync.then(
() => (settled = true),
() => (settled = true),
)
await arrived.query.promise
vi.advanceTimersByTime(600)
releaseQuery()
await arrived.update.promise
// 600 ms of the 1000 ms are spent. One more turn of the event loop must not end the sync.
vi.advanceTimersByTime(399)
await new Promise((resolve) => setImmediate(resolve))
expect(settled).toBe(false)
// The 1000th millisecond ends it; a deadline restarted for the update would still have
// 600 ms left, and the test would stop at vitest's hang bound instead.
vi.advanceTimersByTime(1)
await expect(sync).rejects.toThrow("Convex platform service environment sync failed")
expect(requests).toEqual(["/api/query", "/api/update_environment_variables"])
} finally {
vi.useRealTimers()
}
})
it("aborts a stalled response body instead of waiting indefinitely", async () => {
// As above, the test owns the deadline's clock: time passes only once the stalled body has
// been written, however long the host takes to get the request there.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] })
try {
let requests = 0
const written = signal()
const url = await listen((_req, res) => {
requests++
res.writeHead(200, { "Content-Type": "application/json" })
res.write('{"status":"success","value":', () => written.resolve())
})
const sync = syncPlatformEnvironment({
url,
adminKey: "fixture-placeholder",
variables: { NEW: "fixture-new" },
timeoutMs: 50,
})
await written.promise
vi.advanceTimersByTime(50)
await expect(sync).rejects.toThrow("Convex platform service environment sync failed")
expect(requests).toBe(1)
} finally {
vi.useRealTimers()
}
})
it("refuses redirects before any credential can reach another host", async () => {
let foreignRequests = 0
const foreign = await listen((_req, res) => {
foreignRequests++
res.writeHead(200).end()
})
const url = await listen((_req, res) => res.writeHead(307, { Location: foreign }).end())
await expect(
syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables: { NEW: "fixture-new" } }),
).rejects.toThrow("Convex platform service environment sync failed")
expect(foreignRequests).toBe(0)
})
})
+321
View File
@@ -0,0 +1,321 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The Convex profile's live stream in the web process (apps/web/src/server/convex-live.ts) and the write
// txid of its procedure transport (convex-caller.ts, umbrella TS6.3), against fake Convex clients: the
// log source reads `live:head`/`live:changes` and subscribes to `live:head` once per organization while a
// stream of it is open; `live:open`'s refusals are the live problems the Postgres profile answers (401
// signed out, 404 not reached) and its denials pass through; the heartbeat asks `live:recheck`; a write
// is called with `conusaiLiveTxid` and its envelope's txid becomes the request's `ConusAI-Txid`.
// The Convex functions themselves (convex/live.ts) run in a deployment; this file does not start one.
import { captureTxids } from "@conusai-kit/data-kysely"
import { convexLiveLog, createLiveServer, LiveProblem, type OpenRequest } from "@conusai-kit/live-server"
import { getFunctionName, type UserIdentityAttributes } from "convex/server"
import { ConvexError } from "convex/values"
import { describe, expect, it, vi } from "vitest"
/** Every call a fake ConvexHttpClient of convex-caller.ts received: [kind, function, args]. */
const httpCalls: Array<[string, string, Record<string, unknown>]> = []
/** What that client answers; a test sets `answer` for the calls it makes. */
const http: { answer: (kind: string, name: string, args: Record<string, unknown>) => unknown } = { answer: () => null }
vi.mock("convex/browser", () => {
class FakeHttpClient {
setAdminAuth(): void {}
async query(reference: never, args: Record<string, unknown>) {
httpCalls.push(["query", getFunctionName(reference), args])
return http.answer("query", getFunctionName(reference), args)
}
async mutation(reference: never, args: Record<string, unknown>) {
httpCalls.push(["mutation", getFunctionName(reference), args])
return http.answer("mutation", getFunctionName(reference), args)
}
async action(reference: never, args: Record<string, unknown>) {
httpCalls.push(["action", getFunctionName(reference), args])
return http.answer("action", getFunctionName(reference), args)
}
}
return { ConvexHttpClient: FakeHttpClient, ConvexClient: class {} }
})
const { convexLiveAuthorize, convexLiveSource } = await import("../apps/web/src/server/convex-live")
const { unwrapLiveTxid } = await import("../apps/web/src/server/convex-caller")
const { createConvexCaller } = await import("../apps/web/src/server/convex-caller") // kit:notes
const ORG = "org-1"
const HEAD = { txid: 4, prunedThrough: 0 }
/** A fake subscribing client: what is subscribed now, and a push of a new head. */
function fakeWatcher() {
const subscriptions = new Map<number, { orgId: string; callback: (value: unknown) => unknown }>()
let next = 0
const client = {
opened: 0,
closed: 0,
subscriptions,
onUpdate(reference: never, args: never, callback: (value: unknown) => unknown) {
expect(getFunctionName(reference)).toBe("live:head")
const id = next++
subscriptions.set(id, { orgId: (args as { orgId: string }).orgId, callback })
callback(HEAD)
return () => subscriptions.delete(id)
},
async close() {
client.closed += 1
},
push(orgId: string, head: unknown) {
for (const entry of subscriptions.values()) if (entry.orgId === orgId) entry.callback(head)
},
}
return client
}
function fakeSource() {
const watcher = fakeWatcher()
const reads: Array<[string, Record<string, unknown>]> = []
const source = convexLiveSource({
admin: {
async query(reference: never, args: never) {
const name = getFunctionName(reference)
reads.push([name, args as Record<string, unknown>])
if (name === "live:head") return HEAD
if (name === "live:changes") return { rows: [], head: HEAD, more: false }
throw new Error(`unexpected ${name}`)
},
},
watcher: () => {
watcher.opened += 1
return watcher
},
})
return { source, watcher, reads }
}
const identityOf = (userId: string): UserIdentityAttributes => ({
issuer: "https://conusai.internal/bff",
subject: userId,
tokenIdentifier: `https://conusai.internal/bff|${userId}`,
})
const OPEN: OpenRequest = {
protocol: "clp/1",
after: null,
entities: ["notes.note", "notes.secret"],
topics: [{ ws: "main", project: "notes" }],
}
const request = () => new Request(`http://app.test/api/v1/orgs/acme/live`, { method: "POST" })
const problem = (status: number, code: string) =>
new ConvexError({ type: `/problems/${code}`, title: code, status, code })
/** The authorize of convex-live.ts over a fake caller and a fake `live:open`/`live:recheck`. */
function authorizeWith(answer: (name: string, args: Record<string, unknown>) => unknown, userId = () => "u-ada") {
const calls: Array<[string, UserIdentityAttributes, Record<string, unknown>]> = []
const warn = vi.fn()
const authorize = convexLiveAuthorize({
caller: async () => ({ userId: userId(), session: "s-1", identity: identityOf(userId()) }),
clientFor: (identity) => ({
async query(reference: never, args: never) {
const name = getFunctionName(reference)
calls.push([name, identity, args as Record<string, unknown>])
return answer(name, args as Record<string, unknown>)
},
}),
orgSlugOf: () => "acme",
warn,
})
return { authorize, calls, warn }
}
const granted = {
org: ORG,
topics: [{ ws: "main", project: "notes", ids: { org: ORG, ws: "w-1", project: "p-1" } }],
entities: ["notes.note"],
denied: [{ entity: "notes.secret", reason: "unknown" }],
}
describe("the Convex live log source", () => {
it("reads the head and the changes with the admin client, and positions them as convexLiveLog does", async () => {
const { source, reads } = fakeSource()
const log = convexLiveLog({ source })
expect(await log.head(ORG)).toEqual({ position: { txid: 5n, seq: 0n }, w: 5n })
const read = await log.read(ORG, { txid: 3n, seq: 2n }, 50)
expect(read).toMatchObject({ final: [], more: false, w: 5n, position: { txid: 5n, seq: 0n } })
expect(reads).toEqual([
["live:head", { orgId: ORG }],
["live:changes", { orgId: ORG, after: { txid: 3, seq: 2 }, limit: 50 }],
])
})
it("subscribes to an organization's head once while its streams are open, and ends it with the last one", async () => {
const { source, watcher } = fakeSource()
const log = convexLiveLog({ source })
const woken: Array<string | null> = []
log.onWake((org) => woken.push(org))
const first = log.watch?.(ORG)
const second = log.watch?.(ORG)
const other = log.watch?.("org-2")
// One websocket client, one subscription per organization.
expect(watcher.opened).toBe(1)
expect([...watcher.subscriptions.values()].map(({ orgId }) => orgId)).toEqual([ORG, "org-2"])
woken.length = 0
watcher.push(ORG, { txid: 5, prunedThrough: 0 })
expect(woken).toEqual([ORG])
first?.()
first?.()
expect(watcher.subscriptions.size).toBe(2)
second?.()
expect([...watcher.subscriptions.values()].map(({ orgId }) => orgId)).toEqual(["org-2"])
other?.()
expect(watcher.subscriptions.size).toBe(0)
await log.close()
expect(watcher.closed).toBe(1)
})
it("is what a live server's streams subscribe through: the subscription follows the open streams", async () => {
const { source, watcher } = fakeSource()
const { authorize } = authorizeWith(() => granted)
const server = createLiveServer({
engine: "convex",
log: convexLiveLog({ source }),
authorize,
heartbeatMs: 60_000,
})
const open = async () => {
const abort = new AbortController()
const response = await server.handle(
new Request("http://app.test/api/v1/orgs/acme/live", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ protocol: "clp/1", after: null, entities: ["notes.note"], topics: [] }),
signal: abort.signal,
}),
)
expect(response.status).toBe(200)
const reader = response.body?.getReader()
const first = new TextDecoder().decode((await reader?.read())?.value)
expect(first).toContain("event: hello")
return { abort, reader }
}
const a = await open()
const b = await open()
expect(server.open).toBe(2)
expect(watcher.subscriptions.size).toBe(1)
a.abort.abort()
expect(watcher.subscriptions.size).toBe(1)
b.abort.abort()
expect(watcher.subscriptions.size).toBe(0)
expect(server.open).toBe(0)
await server.close()
expect(watcher.closed).toBe(1)
})
})
describe("the Convex profile's stream authorization", () => {
it("asks live:open as the caller and passes its grants and denials through", async () => {
const { authorize, calls } = authorizeWith(() => granted)
const grant = await authorize(request(), {
...OPEN,
topics: [{ ws: "main", project: "notes", extra: "dropped" } as never],
})
expect(grant.org).toBe(ORG)
expect(grant.session).toBe("s-1")
expect(grant.granted).toEqual({ entities: ["notes.note"], topics: granted.topics })
expect(grant.denied).toEqual([{ entity: "notes.secret", reason: "unknown" }])
expect(calls).toEqual([
[
"live:open",
identityOf("u-ada"),
{ org: "acme", topics: [{ ws: "main", project: "notes" }], entities: ["notes.note", "notes.secret"] },
],
])
})
it("answers live:open's refusals as the Postgres profile does: 401 signed out, 404 not reached", async () => {
for (const [status, type] of [
[401, "urn:conusai:live:unauthenticated"],
[404, "urn:conusai:live:not-found"],
] as const) {
const { authorize } = authorizeWith(() => {
throw problem(status, status === 401 ? "unauthenticated" : "not_found")
})
const refused = await authorize(request(), OPEN).catch((error: unknown) => error)
expect(refused).toBeInstanceOf(LiveProblem)
expect(refused).toMatchObject({ status, type })
}
// Another problem of the kernel's stays the kernel's (serveLive answers it as a procedure's).
const { authorize } = authorizeWith(() => {
throw problem(429, "rate_limited")
})
await expect(authorize(request(), OPEN)).rejects.toMatchObject({ status: 429, code: "rate_limited" })
})
it("re-checks every heartbeat with live:recheck, and revokes a changed caller, a lost membership or a lost read", async () => {
let recheck: () => unknown = () => null
let user = "u-ada"
const { authorize, calls, warn } = authorizeWith(
(name) => (name === "live:open" ? granted : recheck()),
() => user,
)
const grant = await authorize(request(), OPEN)
expect(await grant.recheck()).toBeNull()
expect(calls.at(-1)).toEqual([
"live:recheck",
identityOf("u-ada"),
// The entities the stream was granted, so the deployment re-checks each one's read permission.
{ org: "acme", topics: [{ ws: "main", project: "notes" }], granted: ["notes.note"] },
])
recheck = () => "grants_changed"
expect(await grant.recheck()).toBe("grants_changed")
recheck = () => "membership"
expect(await grant.recheck()).toBe("membership")
recheck = () => {
throw problem(404, "not_found")
}
expect(await grant.recheck()).toBe("membership")
recheck = () => {
throw problem(401, "unauthenticated")
}
expect(await grant.recheck()).toBe("session")
recheck = () => {
throw new Error("the deployment is unreachable")
}
expect(await grant.recheck()).toBeNull()
expect(warn).toHaveBeenCalledWith("live: the re-check failed: Error")
user = "u-grace"
expect(await grant.recheck()).toBe("session")
})
})
describe("the Convex profile's write txid", () => {
it("unwraps a tenant mutation's envelope and records its txid with the router's capture", async () => {
const written = await captureTxids(async () => unwrapLiveTxid({ value: { id: "n-1" }, txid: "12" }))
expect(written).toEqual({ value: { id: "n-1" }, txid: "12" })
const nothing = await captureTxids(async () => unwrapLiveTxid({ value: null, txid: null }))
expect(nothing).toEqual({ value: null, txid: null })
expect(() => unwrapLiveTxid({ id: "n-1" })).toThrow()
})
// kit:notes:begin
it("asks a write for its txid and answers it as ConusAI-Txid's capture; a read is called unchanged", async () => {
httpCalls.length = 0
http.answer = (kind) => (kind === "mutation" ? { value: { noteId: "n-1" }, txid: "7" } : { items: [] })
const caller = createConvexCaller({
url: "http://convex.test",
adminKey: "admin-key",
principal: { actor: { userId: "u-ada" }, method: "bearer" } as never,
objectStore: {} as never,
identity: identityOf("u-ada"),
})
const input = { org: "acme", workspace: "main", project: "notes", title: "A" }
const write = await captureTxids(() => caller.callByName("notes.note.create", input))
expect(write).toEqual({ value: { noteId: "n-1" }, txid: "7" })
const read = await captureTxids(() => caller.callByName("notes.note.list", { org: "acme" }))
expect(read).toEqual({ value: { items: [] }, txid: null })
expect(httpCalls).toEqual([
["mutation", "generated/notes:notes_note_create", { input, conusaiLiveTxid: true }],
["query", "generated/notes:notes_note_list", { input: { org: "acme" } }],
])
})
// kit:notes:end
})
+637
View File
@@ -0,0 +1,637 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { readFileSync } from "node:fs"
import { AppError } from "@conusai-kit/core"
import { getFunctionName } from "convex/server"
import { ConvexError } from "convex/values"
import { describe, expect, it, vi } from "vitest"
import { parse } from "yaml"
import { projectEntriesFor } from "../apps/web/src/server/project-entries.js" // kit:documents
import {
CONVEX_ACTION_EFFECT_MODULES,
CONVEX_EXCLUDED_MODULES,
CONVEX_EXCLUDED_PROCEDURES,
convexBffModules,
convexModules,
} from "../convex/lib/convexModules.mjs"
import { appModules } from "../modules/index.js"
const root = new URL("..", import.meta.url)
describe("Convex deployable module closure", () => {
// kit:documents:begin
it("keeps the documents review route visible in the Convex project navigation", () => {
expect(projectEntriesFor("convex")).toContainEqual({
href: "documents",
label: "documents.title",
module: "documents",
})
})
// kit:documents:end
it("includes server-side auth handlers and only excludes modules with an explicit runtime gap", () => {
// Modules that declare `profiles: ["postgres"]` (knowledge, agent) ship fail-closed Convex
// binding files, but the integration review (2026-10-23) kept them Postgres-only: their
// search/run effects and jobs leave the process and have no composed Convex Node actions yet
// (the reasons are in their module READMEs). The generated closure is therefore the modules
// that support the profile.
const convexBound = appModules.filter(
(module) => module.profiles === undefined || module.profiles.includes("convex"),
)
const modules = convexModules(convexBound)
expect(modules.map(({ id }) => id)).toEqual([
"tenancy",
"auth",
"files",
"proposals",
"notifications",
"branding",
"notes", // kit:notes
"account",
"documents", // kit:documents
"guides", // kit:guides
"support", // kit:support
])
// The only effects generated as actions are those of the platform-call modules (convex/lib/effects.ts).
const effects = modules.flatMap(({ id, procedures = [] }) =>
procedures.filter(({ kind }) => kind === "effect").map(({ name }) => [id, name] as const),
)
expect(effects.every(([id]) => CONVEX_ACTION_EFFECT_MODULES.has(id))).toBe(true)
const effectNames = effects.map(([, name]) => name)
// The account module's export and deletion: actions over its bound store (convex/account.ts).
const expectedEffects: string[] = [
"account.data.export",
"account.deletion.request",
"account.analytics.request",
"account.analytics.status",
"account.analytics.export",
]
// kit:guides:begin
expectedEffects.push(
"guides.builder.open",
"guides.runtime.bootstrap",
"guides.runtime.deleteFile",
"guides.runtime.record",
)
// A survey file's upload is a streamed body: not a Convex action on this profile.
expect(CONVEX_EXCLUDED_PROCEDURES.has("guides.runtime.uploadFile")).toBe(true)
// kit:guides:end
// kit:support:begin
expectedEffects.push("support.widgetSession.mint", "support.widget.config")
// kit:support:end
expect([...effectNames].sort()).toEqual(expectedEffects.sort())
const fileProcedures = modules.find(({ id }) => id === "files")?.procedures?.map(({ name }) => name)
expect(fileProcedures ?? []).toContain("files.file.get")
expect(fileProcedures ?? []).toContain("files.file.list")
expect(fileProcedures ?? []).not.toContain("files.file.delete")
expect(CONVEX_EXCLUDED_PROCEDURES.has("files.file.delete")).toBe(true)
expect([...CONVEX_EXCLUDED_MODULES.keys()]).toEqual([])
const branding = modules.find(({ id }) => id === "branding")?.procedures?.map(({ name }) => name)
expect(branding).toEqual(["branding.brand.get", "branding.brand.set"])
for (const name of ["branding.logo.get", "branding.logo.set", "branding.logo.clear"]) {
expect(CONVEX_EXCLUDED_PROCEDURES.has(name)).toBe(true)
expect(
convexBffModules(appModules)
.flatMap(({ procedures = [] }) => procedures)
.map(({ name }) => name),
).not.toContain(name)
}
expect(CONVEX_EXCLUDED_MODULES.get("auth")).toBeUndefined()
})
it("keeps composed file effects in the web BFF registry without generating Convex effect wrappers", () => {
const filtered = appModules.filter((module) => module.profiles === undefined || module.profiles.includes("convex"))
const backend = convexModules(filtered)
const bff = convexBffModules(filtered)
expect(
backend.some(
({ id, procedures = [] }) =>
!CONVEX_ACTION_EFFECT_MODULES.has(id) && procedures.some(({ kind }) => kind === "effect"),
),
).toBe(false)
const fileProcedures = bff.find(({ id }) => id === "files")?.procedures?.map(({ name }) => name)
expect(fileProcedures ?? []).toContain("files.file.beginUpload")
expect(fileProcedures ?? []).toContain("files.file.putPart")
expect(fileProcedures ?? []).toContain("files.file.completeUpload")
expect(fileProcedures ?? []).toContain("files.file.delete")
expect(bff.find(({ id }) => id === "proposals")?.procedures?.map(({ name }) => name)).toContain(
"proposals.proposal.approve",
)
// kit:documents:begin
expect(bff.find(({ id }) => id === "documents")?.procedures?.map(({ name }) => name)).toContain(
"documents.document.get",
)
// kit:documents:end
})
it("registers scheduled mutations through generated internal function references", () => {
const crons = readFileSync(new URL("convex/crons.ts", root), "utf8")
expect(crons).toContain("internal.relay.relayPass")
expect(crons).toContain("internal.files.recoverRows")
// The account module's deletion sweep, the Postgres worker's DELETION_SWEEP_JOB.
expect(crons).toContain("internal.account.sweep")
expect(crons).not.toContain('from "./relay"')
})
it("keeps audit credentials behind a root internal action", () => {
const config = readFileSync(new URL("convex/convex.config.ts", root), "utf8")
const provider = readFileSync(new URL("convex/auditCredentials.ts", root), "utf8")
const relay = readFileSync(new URL("convex/relay.ts", root), "utf8")
expect(config).not.toContain("CONUSAI_API_TOKEN")
expect(provider).toContain("export const get = internalAction(")
expect(provider).toContain("apiUrl: process.env.CONUSAI_API_URL")
expect(provider).toContain("apiToken: process.env.CONUSAI_API_TOKEN")
expect(relay).toContain("credentialsProvider: internal.auditCredentials.get")
})
it("deploys functions before web and backup with the provider's server-only credential names", () => {
const compose = parse(readFileSync(new URL("profiles/convex/compose.yaml", root), "utf8")) as {
services: Record<
string,
{
command?: string[]
environment?: Record<string, string>
restart?: string
depends_on?: Record<string, { condition: string }>
}
>
}
const profile = JSON.parse(readFileSync(new URL("profiles/convex/conusai.json", root), "utf8")) as {
services: Record<string, unknown>
}
expect(Object.keys(compose.services).sort()).toEqual(["backup", "convex-deploy", "web"])
expect(Object.keys(profile.services).sort()).toEqual(Object.keys(compose.services).sort())
expect(compose.services["convex-deploy"]?.command).toEqual(["node", "profiles/convex/deploy.mjs"])
expect(compose.services["convex-deploy"]?.restart).toBe("no")
for (const name of ["web", "backup"] as const) {
expect(compose.services[name]?.depends_on?.["convex-deploy"]).toEqual({
condition: "service_completed_successfully",
})
}
for (const name of ["web", "backup", "convex-deploy"] as const) {
const environment = compose.services[name]?.environment ?? {}
expect(environment.CONVEX_SELF_HOSTED_URL).toBe("$" + "{CONVEX_SELF_HOSTED_URL}")
expect(environment.CONVEX_SELF_HOSTED_ADMIN_KEY).toBe("$" + "{CONVEX_SELF_HOSTED_ADMIN_KEY}")
// `VITE_` is what Vite writes into the browser's bundle.
expect(Object.keys(environment).some((key) => key.startsWith("VITE_") && key.includes("ADMIN"))).toBe(false)
expect(environment.CONVEX_URL).toBeUndefined()
expect(environment.CONVEX_ADMIN_KEY).toBeUndefined()
}
expect(compose.services["convex-deploy"]?.environment).toMatchObject({
CONUSAI_API_TOKEN: expect.stringMatching(/^\$\{CONUSAI_API_TOKEN\}$/),
CONUSAI_PROJECT_ID: expect.stringMatching(/^\$\{CONUSAI_PROJECT_ID\}$/),
CONUSAI_ORG_SLUG: expect.stringMatching(/^\$\{CONUSAI_ORG_SLUG\}$/),
})
expect(compose.services.web?.environment?.CONUSAI_ORG_SLUG).toBe("$" + "{CONUSAI_ORG_SLUG}")
const deploy = readFileSync(new URL("profiles/convex/deploy.mjs", root), "utf8")
expect(deploy).toContain("await syncPlatformEnvironment({")
expect(deploy).toContain('adminKey: required("CONVEX_SELF_HOSTED_ADMIN_KEY")')
expect(deploy).toContain('"deploy", "--typecheck", "disable"')
const runtime = readFileSync(new URL("convex/lib/runtime.ts", root), "utf8")
expect(runtime).toContain('"filesActions:recoverUpload"')
})
})
// kit:guides:begin
// The deprecated deploy-time publish step of each profile (compose how-to §4a): a stack without
// guides.bundle hands the short-lived credential to the one service whose conusai.json entry declares
// `"guides": { "publish": true }`, and only when it is a one-shot job that is not public.
describe("the Guides publish step of each profile", () => {
const read = (path: string) => readFileSync(new URL(path, root), "utf8")
type Service = {
public?: unknown
guides?: { publish?: boolean }
restart?: string
build?: { dockerfile?: string; target?: string }
command?: string[]
depends_on?: Record<string, { condition?: string }>
}
const profiles = [
{
name: "postgres",
dir: "",
step: "migrate",
entry: "apps/worker/src/migrate.ts",
runs: "apps/worker/dist/migrate.js",
},
{
name: "convex",
dir: "profiles/convex/",
step: "convex-deploy",
entry: "profiles/convex/deploy.mjs",
runs: "profiles/convex/deploy.mjs",
},
] as const
for (const profile of profiles) {
it(`${profile.name}: fresh default-off requests no publisher and web waits for the private one-shot`, () => {
const config = JSON.parse(read(`${profile.dir}conusai.json`)) as { services: Record<string, Service> }
const declared = Object.entries(config.services).filter(([, service]) => service.guides?.publish === true)
expect(declared.map(([name]) => name)).toEqual([])
// Selection grants no Cloud readiness; the packed matrix separately checks explicit-on code guides.
expect(config.services[profile.step]?.public).toBeUndefined()
const composeText = read(`${profile.dir}compose.yaml`)
const compose = parse(composeText) as { services: Record<string, Service> }
const step = compose.services[profile.step]
expect(step?.restart).toBe("no")
expect(step?.build).toMatchObject({ dockerfile: "Dockerfile", target: "runtime" })
expect(step?.command).toEqual(["node", profile.runs])
expect(compose.services.web?.depends_on?.[profile.step]).toEqual({ condition: "service_completed_successfully" })
expect(composeText).not.toMatch(/CONUSAI_GUIDES_(PUBLISH_TOKEN|DEPLOYMENT)/)
})
it(`${profile.name}: the step can publish the code guides and the image's anchor catalogue`, () => {
const source = read(profile.entry)
expect(source).toContain("publishGuidesAtDeploy")
for (const name of ["CONUSAI_API_URL", "CONUSAI_GUIDES_PUBLISH_TOKEN", "CONUSAI_GUIDES_DEPLOYMENT"]) {
expect(source).toContain(`${name}: process.env.${name}`)
}
const relative = /new URL\("([^"]*guides-anchors\.json)", import\.meta\.url\)/.exec(source)?.[1]
expect(relative, `${profile.entry} reads no guides-anchors.json`).toBeDefined()
expect(new URL(relative as string, new URL(`file:///app/${profile.runs}`)).pathname).toBe(
"/app/guides-anchors.json",
)
})
}
it("the image both steps run writes the anchor catalogue with kit guides:anchors at /app", () => {
const dockerfile = read("Dockerfile")
const build = dockerfile.slice(
dockerfile.indexOf("FROM deps AS build"),
dockerfile.indexOf("FROM node:24-slim AS runtime"),
)
expect(build).toMatch(/^WORKDIR \/app$/m)
expect(build).toMatch(/^RUN pnpm kit guides:anchors --out guides-anchors\.json/m)
expect(build.indexOf("guides:anchors")).toBeGreaterThan(build.indexOf("pnpm -r run build"))
expect(build.indexOf("guides:anchors")).toBeLessThan(build.indexOf("pnpm prune --prod"))
expect(dockerfile).toContain("COPY --from=build --chown=root:root /app /app")
})
})
// kit:guides:end
describe("the account module on the Convex profile", () => {
it("binds a store whose every method is one internal query (a read) or mutation (a change)", async () => {
const { ACCOUNT_STORE_READS, ACCOUNT_STORE_WRITES, convexAccountStore } = await import("../convex/lib/effects")
const { createAccountStore } = await import("../modules/account/index.js")
const methods = Object.keys(createAccountStore({ data: {} as never, system: {} as never })).sort()
expect([...ACCOUNT_STORE_READS, ...ACCOUNT_STORE_WRITES].sort()).toEqual(methods)
const calls: Array<[string, string, unknown]> = []
const record = (kind: string) => async (reference: unknown, args: unknown) => {
calls.push([kind, getFunctionName(reference as never), args])
return null
}
const store = convexAccountStore({
auth: { getUserIdentity: async () => null },
runQuery: record("query") as never,
runMutation: record("mutation") as never,
})
await store.deletion("user-1")
await store.identities("user-1")
await store.changeDeletion("user-1", { kind: "step", step: "credentials" })
expect(calls).toEqual([
["query", "account:storeRead", { method: "deletion", args: ["user-1"] }],
["query", "account:storeRead", { method: "identities", args: ["user-1"] }],
[
"mutation",
"account:storeWrite",
{ method: "changeDeletion", args: ["user-1", { kind: "step", step: "credentials" }] },
],
])
})
it("brings a backend refusal back as the kernel's own problem", async () => {
const { convexAccountStore } = await import("../convex/lib/effects")
const store = convexAccountStore({
auth: { getUserIdentity: async () => null },
runQuery: (async () => {
throw new ConvexError({ type: "/problems/not_found", title: "Not found", status: 404, code: "not_found" })
}) as never,
runMutation: (async () => null) as never,
})
const error = await store.deletion("user-1").then(
() => null,
(thrown: unknown) => thrown,
)
expect(error).toBeInstanceOf(AppError)
expect((error as AppError).status).toBe(404)
})
it("binds the real platform privacy adapter in account actions", async () => {
const { effectContainer } = await import("../convex/lib/effects")
// The token the deployment binds: effects.ts resolves the built modules, and a token is
// matched by identity, so the source module's token would be a different one.
const { ACCOUNT_PRIVACY } = await import("../modules/dist/account/index.js")
const env = { ...process.env }
const calls: string[] = []
const response = {
id: "00000000-0000-4000-8000-000000000001",
kind: "erase",
status: "completed",
stores: {},
created_at: "2026-10-28T00:00:00Z",
}
const fetchMock = async (url: string | URL | Request) => {
calls.push(String(url))
return new Response(JSON.stringify(response), { status: 202 })
}
process.env.CONUSAI_API_URL = "https://api.example.test/api"
process.env.CONUSAI_API_TOKEN = "server-only-token"
delete process.env.CONUSAI_FAKE_PLATFORM
try {
const originalFetch = globalThis.fetch
globalThis.fetch = fetchMock as typeof fetch
try {
const container = effectContainer({
auth: { getUserIdentity: async () => null },
runQuery: (async () => null) as never,
runMutation: (async () => null) as never,
})
await container.resolve(ACCOUNT_PRIVACY).eraseSubject({ userId: "user-1" })
} finally {
globalThis.fetch = originalFetch
}
} finally {
process.env = env
}
expect(calls).toEqual(["https://api.example.test/api/v1/privacy/requests"])
})
it("reads the account credential from the BFF's identity claims, and nothing from an absent one", async () => {
const { accountCredentialOf } = await import("../convex/lib/runtime")
const identity = (claims: Record<string, unknown>) =>
({ issuer: "https://conusai.internal/bff", subject: "u", tokenIdentifier: "t", ...claims }) as never
expect(accountCredentialOf(null)).toEqual({ interactive: false })
expect(accountCredentialOf(identity({}))).toEqual({ interactive: false })
expect(accountCredentialOf(identity({ conusaiCredential: "interactive", conusaiSessionId: "s-1" }))).toEqual({
interactive: true,
sessionId: "s-1",
})
expect(
accountCredentialOf(
identity({ conusaiCredential: "interactive", conusaiSignedInAt: "2026-10-25T00:00:00.000Z" }),
),
).toEqual({ interactive: true, signedInAt: "2026-10-25T00:00:00.000Z" })
expect(accountCredentialOf(identity({ conusaiCredential: true, conusaiSessionId: "s-1" }))).toEqual({
interactive: false,
})
})
// kit:convex-better-auth:begin
it("installs the Better Auth deployment's variables in the backend only for a Better Auth app", () => {
const deploy = readFileSync(new URL("profiles/convex/deploy.mjs", root), "utf8")
expect(deploy).toContain(
'variables.push("AUTH_PROVIDER", "AUTH_IDENTITY_NAMESPACE", "BETTER_AUTH_CONVEX_URL", "BETTER_AUTH_CONVEX_ADMIN_KEY")',
)
})
// kit:convex-better-auth:end
})
/**
* The sign-out revocation outbox on the Convex profile (joint contract §14): the Postgres profile's
* own outbox (modules/account/core/revocation.ts) on Convex storage. The web records a revoked app
* session through the internal action `account:revokeSession`, which writes the row and delivers it
* at once from the backend; what a platform did not confirm the one-minute cron `account:revocationSweep`
* retries with the row's backoff, once per module, until 65 minutes have passed. Here the backend
* functions are the very store the Convex functions run, over a memory data source in place of ctx.db.
*/
describe("the revocation outbox on the Convex profile", () => {
it("keeps the outbox in the backend: every store method is one internal query or mutation", async () => {
const { REVOCATION_STORE_READS, REVOCATION_STORE_WRITES, convexRevocationStore } = await import(
"../convex/lib/effects"
)
const { createRevocationStore } = await import("../modules/account/index.js")
expect([...REVOCATION_STORE_READS, ...REVOCATION_STORE_WRITES].sort()).toEqual(
Object.keys(createRevocationStore({} as never)).sort(),
)
const calls: Array<[string, string, unknown]> = []
const record = (kind: string) => async (reference: unknown, args: unknown) => {
calls.push([kind, getFunctionName(reference as never), args])
return kind === "query" ? [] : null
}
const store = convexRevocationStore({
auth: { getUserIdentity: async () => null },
runQuery: record("query") as never,
runMutation: record("mutation") as never,
})
await store.due("2026-10-26T00:00:00.000Z", 10)
await store.purge("2026-10-25T00:00:00.000Z")
expect(calls).toEqual([
["query", "account:revocationRead", { method: "due", args: ["2026-10-26T00:00:00.000Z", 10] }],
["mutation", "account:revocationWrite", { method: "purge", args: ["2026-10-25T00:00:00.000Z"] }],
])
})
it("binds the device session a derived service session is minted for, from the BFF's identity", async () => {
const { requestSessionOf } = await import("../convex/lib/runtime")
const { effectContainer } = await import("../convex/lib/effects")
const { REQUEST_SESSION } = await import("@conusai-kit/tenancy")
const identity = (claims: Record<string, unknown>) =>
({ issuer: "https://conusai.internal/bff", subject: "u", tokenIdentifier: "t", ...claims }) as never
const fake = process.env.CONUSAI_FAKE_PLATFORM
process.env.CONUSAI_FAKE_PLATFORM = "1"
expect(requestSessionOf(null)).toBeNull()
expect(requestSessionOf(identity({}))).toBeNull()
expect(requestSessionOf(identity({ conusaiIdentitySession: 7 }))).toBeNull()
expect(requestSessionOf(identity({ conusaiIdentitySession: "4:oidc:36:s-1" }))).toBe("4:oidc:36:s-1")
const ctx = {
auth: { getUserIdentity: async () => null },
runQuery: (async () => null) as never,
runMutation: (async () => null) as never,
}
expect(effectContainer(ctx, { interactive: true }, "4:oidc:36:s-1").resolve(REQUEST_SESSION)).toBe("4:oidc:36:s-1")
expect(effectContainer(ctx).resolve(REQUEST_SESSION)).toBeNull()
if (fake === undefined) delete process.env.CONUSAI_FAKE_PLATFORM
else process.env.CONUSAI_FAKE_PLATFORM = fake
// The BFF names the device session of every call it makes for a signed-in person.
const caller = readFileSync(new URL("apps/web/src/server/convex-caller.ts", root), "utf8")
expect(caller).toContain("conusaiIdentitySession: options.identitySession")
const request = readFileSync(new URL("apps/web/src/server/request.ts", root), "utf8")
expect(request).toContain("identitySession: sessionRefKey(session.sessionRef)")
})
it("records a sign-out through an internal action and retries it on a one-minute cron", () => {
const account = readFileSync(new URL("convex/account.ts", root), "utf8")
for (const name of ["revocationRead = internalQuery", "revocationWrite = internalMutation"]) {
expect(account).toContain(`export const ${name}`)
}
expect(account).toContain("export const revokeSession = internalAction")
expect(account).toContain("export const revocationSweep = internalAction")
const crons = readFileSync(new URL("convex/crons.ts", root), "utf8")
expect(crons).toContain(
'crons.interval("account session revocations", { minutes: 1 }, internal.account.revocationSweep, {})',
)
// The web asks the backend's outbox, and falls back to the modules only when nothing was recorded.
const app = readFileSync(new URL("apps/web/src/server/app.ts", root), "utf8")
expect(app).toContain("durable: (session: string) => revokeConvexSession(convex, session)")
})
// kit:support:begin
describe("tenancy's share of an account, from an action", () => {
// An action has no database: tenancy's own share runs in the deployment's functions, as the account
// store's methods do (convex/account.ts), so a Convex export and deletion do not hold at it.
it("is read and erased by internal functions, and every other share is the module's own", async () => {
const { effectContainer } = await import("../convex/lib/effects")
const { accountSharesOf } = await import("../modules/dist/account/index.js")
const { accountDataOf } = await import("@conusai-kit/tenancy")
const { registry } = await import("../convex/lib/runtime")
const calls: unknown[] = []
const answer = (result: unknown) => async (reference: unknown, input: unknown) => {
calls.push([getFunctionName(reference as never), input])
return result
}
const ctx = {
auth: { getUserIdentity: async () => null },
runQuery: answer({ customRoles: [], projectMemberships: [], rolesAuthored: [] }) as never,
runMutation: answer(null) as never,
}
const fake = process.env.CONUSAI_FAKE_PLATFORM
process.env.CONUSAI_FAKE_PLATFORM = "1"
try {
const container = effectContainer(ctx)
const shares = accountSharesOf(container)
expect(shares.map(({ key }) => key)).toEqual(accountDataOf(registry).map(({ key }) => key))
const tenancy = shares.find(({ key }) => key === "tenancy")
await expect(tenancy?.export?.({ subject: "ada", container })).resolves.toMatchObject({ rolesAuthored: [] })
await tenancy?.erase?.({ subject: "ada", container })
expect(calls).toEqual([
["account:tenancyShareRead", { subject: "ada" }],
["account:tenancyShareWrite", { subject: "ada" }],
])
} finally {
if (fake === undefined) delete process.env.CONUSAI_FAKE_PLATFORM
else process.env.CONUSAI_FAKE_PLATFORM = fake
}
})
it("is what convex/account.ts runs in those functions, over this function's ctx.db", () => {
const account = readFileSync(new URL("convex/account.ts", root), "utf8")
for (const name of ["tenancyShareRead = internalQuery", "tenancyShareWrite = internalMutation"]) {
expect(account).toContain(`export const ${name}`)
}
})
})
describe("delivered from an action's container, against the fake platform", () => {
const START = Date.parse("2026-10-25T10:00:00.000Z")
type Platform = Awaited<ReturnType<typeof import("@conusai-kit/testing/fake-platform").startFakePlatform>>
type Clock = { at(ms: number): void }
/** One test against a fresh fake platform, the backend's deployment variables and a fixed clock. */
async function withPlatform(run: (platform: Platform, clock: Clock) => Promise<void>): Promise<void> {
const { vi } = await import("vitest")
const { startFakePlatform } = await import("@conusai-kit/testing/fake-platform")
const env = { ...process.env }
const platform = await startFakePlatform()
process.env.CONUSAI_API_URL = platform.url
process.env.CONUSAI_API_TOKEN = "cnsi_test"
process.env.CONUSAI_SUPPORT_CHANNEL = "web"
delete process.env.CONUSAI_FAKE_PLATFORM
vi.useFakeTimers({ toFake: ["Date"] })
vi.setSystemTime(START)
try {
await run(platform, { at: (ms) => vi.setSystemTime(START + ms) })
} finally {
vi.useRealTimers()
process.env = env
await platform.close()
}
}
/** The backend: `account:revocationRead`/`revocationWrite` over one store, as convex/account.ts runs them. */
async function backend() {
const { createMemoryDataSource } = await import("@conusai-kit/data")
const { accountSessionRevocations, createRevocationStore } = await import("../modules/account/index.js")
const data = createMemoryDataSource({ entities: [accountSessionRevocations] })
const store = createRevocationStore(data)
const run = (expected: string) => async (reference: unknown, input: unknown) => {
const name = getFunctionName(reference as never)
if (name !== expected) throw new Error(`unexpected backend call ${name}`)
const { method, args } = input as { method: keyof typeof store; args: unknown[] }
const call = store[method] as (...values: unknown[]) => Promise<unknown>
return JSON.parse(JSON.stringify((await call.apply(store, args)) ?? null))
}
const ctx = {
auth: { getUserIdentity: async () => null },
runQuery: run("account:revocationRead") as never,
runMutation: run("account:revocationWrite") as never,
}
const rows = async () => (await data.global(accountSessionRevocations).list({ limit: 50 })).items
return { ctx, rows }
}
const revocationsOf = async (platform: Platform, session: string) => {
const response = await fetch(new URL("/__e2e/state", platform.url), {
headers: { "x-conusai-organization": "e2e" },
})
const state = (await response.json()) as {
support: Array<{ route: string; body?: { identity_session?: string } }>
}
return state.support.filter((call) => call.route === "revoke" && call.body?.identity_session === session)
}
it("a platform down at sign-out gets the revocation once it is back, and nothing twice", () =>
withPlatform(async (platform, clock) => {
const { revokeSessionInBackend, sweepRevocationsInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c001"
platform.outage("/v1/support/", true)
// The sign-out is not blocked: the action answers once the row is written.
await expect(revokeSessionInBackend(ctx, session)).resolves.toBeUndefined()
expect((await rows())[0]).toMatchObject({ session, state: "pending", pending: ["support"], attempts: 1 })
expect(await revocationsOf(platform, session)).toEqual([])
// Due, still down: the cron's run fails (the line an operator is alerted on), the row waits longer.
clock.at(31_000)
await expect(sweepRevocationsInBackend(ctx)).rejects.toMatchObject({ code: "account_revocations_waiting" })
expect((await rows())[0]).toMatchObject({ state: "pending", attempts: 2 })
platform.outage("/v1/support/", false)
clock.at(31_000 + 61_000)
await sweepRevocationsInBackend(ctx)
expect((await rows())[0]).toMatchObject({ state: "delivered", pending: [], delivered: ["support"] })
expect(await revocationsOf(platform, session)).toHaveLength(1)
clock.at(20 * 60_000)
await sweepRevocationsInBackend(ctx)
expect(await revocationsOf(platform, session)).toHaveLength(1)
// A second notification of the same revocation writes nothing and asks nobody.
await revokeSessionInBackend(ctx, session)
expect(await rows()).toHaveLength(1)
expect(await revocationsOf(platform, session)).toHaveLength(1)
}))
it("delivers at once when the platform is up", () =>
withPlatform(async (platform) => {
const { revokeSessionInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c002"
await revokeSessionInBackend(ctx, session)
expect((await rows())[0]).toMatchObject({ state: "delivered", pending: [] })
expect(await revocationsOf(platform, session)).toHaveLength(1)
}))
it("lapses after 65 minutes, when no widget session it minted can still be alive", () =>
withPlatform(async (platform, clock) => {
const { revokeSessionInBackend, sweepRevocationsInBackend } = await import("../convex/lib/effects")
const { ctx, rows } = await backend()
const session = "18:oidc:0199abcd-1234-7000-8000-00000000c003"
platform.outage("/v1/support/", true)
await revokeSessionInBackend(ctx, session)
platform.outage("/v1/support/", false)
clock.at(66 * 60_000)
await sweepRevocationsInBackend(ctx)
expect((await rows())[0]).toMatchObject({ state: "lapsed" })
expect(await revocationsOf(platform, session)).toEqual([])
}))
})
// kit:support:end
})
// Exercise this closure with bounded, explicit optional admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["documents", "guides", "support"]),
}))
vi.mock("../modules/dist/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/dist/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["documents", "guides", "support"]),
}))
+356
View File
@@ -0,0 +1,356 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A revoked app session ends the service sessions the modules minted for it (joint contract §14): the
// hook the web registers with kit-auth (`onSessionRevoked`) asks every installed module's account
// share to revoke them server-side by the session's reference, bounded, and never fails a sign-out.
// On the Postgres profile it is durable: the account module's revocation outbox records it, and the
// worker's revocation job delivers what a platform that was down did not confirm, exactly once.
import { createSessionStore, deriveKeys, kitSessionRef, sessionRefKey, sessions } from "@conusai-kit/auth"
import { buildRegistry, Container, contribute, defineModule } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA, toInstant } from "@conusai-kit/data"
import { SUPPORT_PLATFORM, supportModule } from "@conusai-kit/support" // kit:support
import { recordingSupportPlatform } from "@conusai-kit/support/testing" // kit:support
import { accountData, accountDataOf } from "@conusai-kit/tenancy"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { revokeDerivedSessions } from "../apps/web/src/server/derived-sessions"
import { ACCOUNT_REGISTRY, accountSessionRevocations, recordSessionRevocation } from "../modules/account/index.js"
import { custodyModule } from "../modules/custody.js"
import { appModules as baseModules } from "../modules/index.js"
const appModules = [...baseModules.filter((entry) => entry.id !== "support"), custodyModule(supportModule)]
const registry = buildRegistry([...appModules])
const ref = { provider: "oidc", opaqueId: "0199abcd-1234-7000-8000-00000000d001" }
function ports(bind: (root: Container) => void, timeoutMs?: number, using = registry) {
const root = new Container()
bind(root)
const warnings: string[] = []
return {
ports: {
registry: using,
root,
log: { warn: (message: string) => warnings.push(message) },
...(timeoutMs ? { timeoutMs } : {}),
},
warnings,
}
}
describe("revokeDerivedSessions", () => {
it("answers at once when no installed module mints derived sessions", async () => {
const without = buildRegistry(appModules.filter((module) => module.id !== "support"))
const none = ports(() => undefined, undefined, without)
await expect(revokeDerivedSessions(none.ports, ref)).resolves.toBeUndefined()
expect(none.warnings).toEqual([])
expect(sessionRefKey(ref)).toContain(ref.opaqueId)
})
// kit:support:begin
it("revokes the support widget sessions minted for exactly that app session", async () => {
const support = recordingSupportPlatform()
const { ports: bound, warnings } = ports((root) => root.value(SUPPORT_PLATFORM, support))
await revokeDerivedSessions(bound, ref)
expect(support.calls).toEqual([
{ method: "revokeWidgetSessions", subject: { subject: "", identitySession: sessionRefKey(ref) } },
])
expect(warnings).toEqual([])
})
it("never fails the sign-out: an unreachable or slow module is logged by name and the hook answers", async () => {
const support = recordingSupportPlatform()
const client = support.client as { revokeWidgetSessions: unknown }
client.revokeWidgetSessions = async () => {
throw new TypeError("fetch failed")
}
const down = ports((root) => root.value(SUPPORT_PLATFORM, support))
await expect(revokeDerivedSessions(down.ports, ref)).resolves.toBeUndefined()
expect(down.warnings).toEqual(["auth: support did not revoke the sessions of a revoked app session"])
client.revokeWidgetSessions = () => new Promise(() => undefined)
const slow = ports((root) => root.value(SUPPORT_PLATFORM, support), 50)
await expect(revokeDerivedSessions(slow.ports, ref)).resolves.toBeUndefined()
expect(slow.warnings).toHaveLength(1)
})
it("asks the modules directly when the revocation could not be recorded", async () => {
const support = recordingSupportPlatform()
const { ports: bound, warnings } = ports((root) => root.value(SUPPORT_PLATFORM, support))
await revokeDerivedSessions(
{
...bound,
durable: async () => {
throw new Error("the database refused the write")
},
},
ref,
)
expect(support.calls).toEqual([
{ method: "revokeWidgetSessions", subject: { subject: "", identitySession: sessionRefKey(ref) } },
])
expect(warnings).toEqual([
"auth: the revocation of an app session could not be recorded; asking the modules directly",
])
})
it("does nothing on the fake platform, where no widget session was minted", async () => {
const offline = ports((root) => root.value(SUPPORT_PLATFORM, { mode: "offline" }))
await expect(revokeDerivedSessions(offline.ports, ref)).resolves.toBeUndefined()
expect(offline.warnings).toEqual([])
})
// kit:support:end
it("asks only the modules that mint derived sessions", () => {
const revoking = accountDataOf(registry)
.filter((entry) => entry.revokeSessions)
.map((entry) => entry.key)
const expected: string[] = []
expected.push("support") // kit:support
expect(revoking).toEqual(expected)
})
})
/**
* The durable path, end to end on the memory data source: kit-auth's own session store signs a
* device out and calls the web's hook, which records the revocation in the account module's outbox;
* the worker's revocation job (the registry's own handler) delivers it. `minting` stands for any
* module that mints derived sessions, and its platform can be taken down and brought back.
*/
describe("the revocation outbox", () => {
const platform = { down: false, delivered: [] as string[], refused: 0 }
const minting = defineModule({
id: "minting",
version: "0.1.0",
kit: "^0.1.0",
requires: ["tenancy"],
extends: [
contribute(accountData, [
{
key: "minting",
async revokeSessions({ session }) {
if (platform.down) {
platform.refused += 1
throw new TypeError("fetch failed")
}
if (session) platform.delivered.push(session)
},
},
]),
],
})
const registry = buildRegistry([...appModules, minting])
const job = registry.jobs["account.sessions.revoke"]
const START = Date.parse("2026-10-25T10:00:00.000Z")
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date"] })
vi.setSystemTime(START)
platform.down = false
platform.delivered.length = 0
platform.refused = 0
})
afterEach(() => {
vi.useRealTimers()
})
async function app() {
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(ACCOUNT_REGISTRY, registry)
root.value(SUPPORT_PLATFORM, { mode: "offline" }) // kit:support
const warnings: string[] = []
const log = { warn: (message: string) => warnings.push(message) }
const store = createSessionStore({
config: {
idleSeconds: 3600,
absoluteSeconds: 86400,
cookieName: "s",
secret: "x".repeat(32),
secureCookies: false,
},
data,
keys: await deriveKeys("x".repeat(32)),
// What apps/web/src/server/app.ts registers on the Postgres profile.
onRevoked: (revoked) =>
revokeDerivedSessions(
{ registry, root, log, durable: (session) => recordSessionRevocation(root, session, { log }) },
revoked,
),
})
const signIn = async (subject: string) =>
(
await store.open({
userId: "0199abcd-1234-7000-8000-00000000e001",
issuer: "https://id.example.test",
subject,
tokens: { accessToken: "a", expiresAt: toInstant(Date.now() + 3600_000) },
})
).row
const runJob = () => {
if (!job) throw new Error("the account module declares no revocation job")
return job.def.handler(null, { container: root.child(), correlationId: "test-revoke", attempt: 1 })
}
const rows = async () => (await data.global(accountSessionRevocations).list({ limit: 50 })).items
const later = (ms: number) => vi.setSystemTime(Date.now() + ms)
return { data, store, signIn, runJob, rows, later, warnings }
}
it("is scheduled by the worker every minute", () => {
expect(job?.module).toBe("account")
})
it("delivers at once when the platform is up, and the job has nothing left to do", async () => {
const { store, signIn, runJob, rows, later } = await app()
const device = await signIn("sub-up")
await store.revoke(device, "sign-out")
const key = sessionRefKey(kitSessionRef(device))
expect(platform.delivered).toEqual([key])
expect((await rows())[0]).toMatchObject({
state: "delivered",
pending: [],
delivered: expect.arrayContaining(["minting"]),
})
later(10 * 60_000)
await runJob()
expect(platform.delivered).toEqual([key])
})
it("a platform down at sign-out gets the revocation once it is back, and nothing twice", async () => {
const { data, store, signIn, runJob, rows, later, warnings } = await app()
const device = await signIn("sub-down")
platform.down = true
// The sign-out is not blocked: it answers, and the app session is revoked.
await expect(store.revoke(device, "sign-out")).resolves.toBeUndefined()
expect((await data.global(sessions).get(device.id))?.revokedAt).not.toBeNull()
expect(platform.delivered).toEqual([])
const [pending] = await rows()
expect(pending).toMatchObject({ state: "pending", pending: ["minting"], attempts: 1, claimedUntil: null })
expect(warnings).toEqual(["auth: minting did not revoke the sessions of a revoked app session; retrying"])
// Not due yet (backoff): the job asks nobody.
await runJob()
expect(platform.refused).toBe(1)
// Due, platform still down: one more refusal, scheduled again with a longer backoff; the run fails.
later(31_000)
await expect(runJob()).rejects.toMatchObject({ code: "account_revocations_waiting" })
expect(platform.refused).toBe(2)
expect((await rows())[0]).toMatchObject({ state: "pending", attempts: 2 })
// Back: the next due run delivers it, once.
platform.down = false
later(61_000)
await runJob()
const key = sessionRefKey(kitSessionRef(device))
expect(platform.delivered).toEqual([key])
expect((await rows())[0]).toMatchObject({
state: "delivered",
pending: [],
delivered: expect.arrayContaining(["minting"]),
})
later(10 * 60_000)
await runJob()
expect(platform.delivered).toEqual([key])
})
it("two runs at once deliver one revocation once", async () => {
const { store, signIn, runJob, later } = await app()
const device = await signIn("sub-race")
platform.down = true
await store.revoke(device, "sign-out")
platform.down = false
later(31_000)
await Promise.all([runJob(), runJob(), runJob()])
expect(platform.delivered).toEqual([sessionRefKey(kitSessionRef(device))])
})
it("gives up once no derived session can still be alive, and removes finished rows a day later", async () => {
const { store, signIn, runJob, rows, later } = await app()
const down = await signIn("sub-lapse")
platform.down = true
await store.revoke(down, "sign-out")
later(66 * 60_000)
await runJob()
const refused = platform.refused
expect((await rows())[0]).toMatchObject({ state: "lapsed" })
later(10 * 60_000)
await runJob()
expect(platform.refused).toBe(refused)
later(25 * 60 * 60_000)
await runJob()
expect(await rows()).toEqual([])
})
})
// kit:profile-convex:begin
/**
* The Convex profile's sign-out (apps/web/src/server/convex-revocation.ts): the web records the revoked
* app session in the backend's outbox through the internal action `account:revokeSession`, with the
* admin key and no identity, bounded. A refusal or a backend that does not answer is thrown, so the
* hook asks the modules directly instead.
*/
describe("the Convex profile's durable revocation", () => {
async function backend(answer: (body: { path: string; args: unknown[] }) => unknown | undefined) {
const { createServer } = await import("node:http")
const seen: Array<{ path: string; args: unknown[]; authorization: string | undefined }> = []
const server = createServer((request, response) => {
let raw = ""
request.on("data", (chunk) => {
raw += chunk
})
request.on("end", () => {
const body = JSON.parse(raw) as { path: string; args: unknown[] }
seen.push({ ...body, authorization: request.headers.authorization })
const value = answer(body)
if (value === undefined) return // never answers
response.writeHead(200, { "content-type": "application/json" })
response.end(JSON.stringify(value))
})
})
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
const address = server.address() as { port: number }
return {
convex: { url: `http://127.0.0.1:${address.port}`, adminKey: "app|admin-key" },
seen,
close: () =>
new Promise<void>((resolve) => {
server.closeAllConnections()
server.close(() => resolve())
}),
}
}
it("records the revocation in the backend's outbox with the admin key", async () => {
const { revokeConvexSession } = await import("../apps/web/src/server/convex-revocation")
const fake = await backend(() => ({ status: "success", value: null, logLines: [] }))
try {
await revokeConvexSession(fake.convex, sessionRefKey(ref))
expect(fake.seen).toMatchObject([
{
path: "account:revokeSession",
args: [{ session: sessionRefKey(ref) }],
authorization: "Convex app|admin-key",
},
])
expect(fake.seen).toHaveLength(1)
} finally {
await fake.close()
}
})
it("throws when nothing was recorded, so the hook asks the modules directly, and never waits long", async () => {
const { revokeConvexSession } = await import("../apps/web/src/server/convex-revocation")
const refused = await backend(() => ({ status: "error", errorMessage: "Server Error", logLines: [] }))
const silent = await backend(() => undefined)
try {
await expect(revokeConvexSession(refused.convex, "s-1")).rejects.toBeDefined()
const started = Date.now()
await expect(revokeConvexSession(silent.convex, "s-2", { timeoutMs: 100 })).rejects.toBeDefined()
expect(Date.now() - started).toBeLessThan(2000)
} finally {
await refused.close()
await silent.close()
}
})
})
// kit:profile-convex:end
+221
View File
@@ -0,0 +1,221 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Run the real dev entry point with synthetic build/database/platform dependencies.
import { type ChildProcess, spawn } from "node:child_process"
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { afterEach, expect, it } from "vitest"
const source = readFileSync(fileURLToPath(new URL("../scripts/dev.mjs", import.meta.url)), "utf8")
let directory: string
let parent: ChildProcess | undefined
const owned = new Set<number>()
const alive = (pid: number) => {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
const delay = () => new Promise((resolve) => setTimeout(resolve, 20))
async function until(check: () => boolean, milliseconds = 5_000) {
const deadline = Date.now() + milliseconds
while (!check()) {
if (Date.now() > deadline) throw new Error("synthetic dev process did not reach the expected state")
await delay()
}
}
afterEach(async () => {
parent?.kill("SIGKILL")
for (const pid of owned) {
try {
process.kill(pid, "SIGKILL")
} catch {
// Only synthetic task-owned children; a completed child may already be gone.
}
}
owned.clear()
if (directory) rmSync(directory, { recursive: true, force: true })
})
function boot(
stage: "migrate" | "seed",
outcome: "wait" | "fail" | "signal" | "missing" | "stubborn" | "orphan" = "wait",
probeError?: "EPERM" | "EIO",
) {
directory = mkdtempSync(join(tmpdir(), "app-dev-shutdown-"))
for (const folder of ["scripts", "bin", "apps/worker/dist"]) mkdirSync(join(directory, folder), { recursive: true })
// Only substitute the external platform dependency; all process management is unchanged.
writeFileSync(
join(directory, "scripts/dev.mjs"),
source.replace('"@conusai-kit/testing/fake-platform"', '"./platform.mjs"'),
)
writeFileSync(
join(directory, "scripts/platform.mjs"),
`
import { writeFileSync } from "node:fs"
export const fakeNativeServices = () => ({ deployment: {} })
export const validReply = () => ({})
export const startFakePlatform = async () => ({
url: "synthetic", ai: { respond() {} },
async close() { writeFileSync(${JSON.stringify(join(directory, "closed"))}, "closed") }
})
`,
)
const pnpm = join(directory, "bin/pnpm")
writeFileSync(pnpm, `#!${process.execPath}\nprocess.exit(0)\n`)
chmodSync(pnpm, 0o755)
if (outcome !== "missing") symlinkSync(process.execPath, join(directory, "bin/node"))
const pidFile = join(directory, "child.pid")
const grandchildFile = join(directory, "grandchild.pid")
const onTerm =
outcome === "stubborn"
? "() => {}"
: `() => setTimeout(() => {
require("node:fs").writeFileSync(${JSON.stringify(join(directory, "child-ended"))}, "ended")
process.exit(0)
}, 30)`
const grandchild = `
${outcome === "stubborn" || outcome === "orphan" ? 'process.on("SIGTERM", () => {});' : ""}
require("node:fs").writeFileSync(${JSON.stringify(grandchildFile)}, String(process.pid))
setInterval(() => {}, 1000)
`
const waiting = `
const { writeFileSync } = require("node:fs")
const { spawn } = require("node:child_process")
process.on("SIGTERM", ${onTerm})
writeFileSync(${JSON.stringify(pidFile)}, String(process.pid))
spawn(process.execPath, ["-e", ${JSON.stringify(grandchild)}], { stdio: "inherit" })
setInterval(() => {}, 1000)
`
const body =
outcome === "fail" ? "process.exit(7)" : outcome === "signal" ? 'process.kill(process.pid, "SIGTERM")' : waiting
writeFileSync(join(directory, "apps/worker/dist/migrate.js"), stage === "migrate" ? body : "process.exit(0)")
writeFileSync(
join(directory, "apps/worker/dist/seed-main.js"),
`require("node:fs").writeFileSync(${JSON.stringify(join(directory, "seed-started"))}, "started");\n${body}`,
)
const probeLog = join(directory, "probes")
const preload = join(directory, "probe.mjs")
if (probeError) {
// Inject only an existence-probe error for a group the actual entry point has signalled.
// Real spawning, TERM delivery, child termination and the production wait bounds remain intact.
writeFileSync(
preload,
`
import { appendFileSync } from "node:fs"
const kill = process.kill.bind(process)
let group
let denied = 0
process.kill = (pid, signal) => {
if (pid === group && signal === 0 && denied < 3) {
denied += 1
appendFileSync(${JSON.stringify(probeLog)}, ${JSON.stringify(`${probeError}\n`)})
throw Object.assign(new Error("synthetic existence probe failure"), { code: ${JSON.stringify(probeError)} })
}
const result = kill(pid, signal)
if (pid < 0 && signal === "SIGTERM") group = pid
return result
}
`,
)
}
const args = [...(probeError ? ["--import", preload] : []), join(directory, "scripts/dev.mjs")]
const child = spawn(process.execPath, args, {
env: {
...process.env,
PATH: join(directory, "bin"),
POSTGRES_URL: "synthetic",
POSTGRES_READER_URL: "synthetic",
POSTGRES_MIGRATE_URL: "synthetic",
},
stdio: ["ignore", "ignore", "pipe"],
})
let stderr = ""
child.stderr?.on("data", (chunk) => {
stderr += String(chunk)
})
parent = child
const exited = new Promise<number | null>((resolve, reject) => {
child.once("exit", resolve)
child.once("error", reject)
})
return { child, exited, pidFile, grandchildFile, probeLog, stderr: () => stderr }
}
it.each([
["migrate", "SIGTERM"],
["seed", "SIGTERM"],
["migrate", "SIGINT"],
["seed", "SIGINT"],
] as const)("%s cancellation by %s stops its child and descendants before further startup", async (stage, signal) => {
const { child, exited, pidFile, grandchildFile } = boot(stage)
await until(() => existsSync(pidFile) && existsSync(grandchildFile))
const pids = [pidFile, grandchildFile].map((file) => Number(readFileSync(file, "utf8")))
for (const pid of pids) owned.add(pid)
child.kill(signal)
expect(await exited).toBe(0)
expect(existsSync(join(directory, "child-ended"))).toBe(true)
await until(() => pids.every((pid) => !alive(pid)))
expect(existsSync(join(directory, "closed"))).toBe(true)
if (stage === "migrate") expect(existsSync(join(directory, "seed-started"))).toBe(false)
})
it.each(["fail", "signal", "missing"] as const)(
"preserves failure handling when the startup child has outcome %s",
async (outcome) => {
const { exited } = boot("migrate", outcome)
expect(await exited).toBe(outcome === "fail" ? 7 : 1)
expect(existsSync(join(directory, "seed-started"))).toBe(false)
},
)
// Use the actual production grace (35s), not a shortened test-only shutdown policy.
it.each(["stubborn", "orphan"] as const)(
"escalates cancellation when the startup process is %s",
async (outcome) => {
const { child, exited, pidFile, grandchildFile } = boot("migrate", outcome)
await until(() => existsSync(pidFile) && existsSync(grandchildFile))
const pids = [pidFile, grandchildFile].map((file) => Number(readFileSync(file, "utf8")))
for (const pid of pids) owned.add(pid)
const sentinel = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { detached: true, stdio: "ignore" })
if (sentinel.pid === undefined) throw new Error("synthetic unrelated process did not start")
owned.add(sentinel.pid)
const began = performance.now()
child.kill("SIGTERM")
await until(() => child.exitCode !== null || child.signalCode !== null, 42_000)
expect(await exited).toBe(0)
expect(pids.every((pid) => !alive(pid))).toBe(true)
expect(performance.now() - began).toBeGreaterThanOrEqual(35_000)
expect(alive(sentinel.pid)).toBe(true)
expect(existsSync(join(directory, "seed-started"))).toBe(false)
expect(existsSync(join(directory, "closed"))).toBe(true)
},
50_000,
)
it.each(["EPERM", "EIO"] as const)("handles existence-probe %s without treating it as absence", async (code) => {
const { child, exited, pidFile, grandchildFile, probeLog, stderr } = boot("migrate", "wait", code)
await until(() => existsSync(pidFile) && existsSync(grandchildFile))
for (const file of [pidFile, grandchildFile]) owned.add(Number(readFileSync(file, "utf8")))
child.kill("SIGTERM")
expect(await exited, stderr()).toBe(code === "EPERM" ? 0 : 1)
expect(existsSync(probeLog)).toBe(true)
if (code === "EPERM") {
// EPERM must keep the group pending, not let cleanup declare it absent immediately.
expect(readFileSync(probeLog, "utf8").trim().split("\n")).toEqual(["EPERM", "EPERM", "EPERM"])
expect(existsSync(join(directory, "closed"))).toBe(true)
}
expect(existsSync(join(directory, "seed-started"))).toBe(false)
})
+114
View File
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The account module's data-subject rights on the mariadb and mongodb profiles: the same store
// (modules/account/adapters/store.ts) over the profile's real data source, with the kernel's and the
// modules' committed dialect migrations applied first. A person's export holds their rows and nobody
// else's, and their deletion erases them and leaves another person's account as it was.
//
// It needs the engines: ACCOUNT_TEST_MARIADB_URL (MariaDB LTS) and ACCOUNT_TEST_MONGODB_URL (MongoDB 8 as
// a single-node replica set, AT-D15), each naming a database of its own that the test may fill. Without
// them each block is skipped by name; `scripts/vendor/test-template.sh app --profile <name>` starts both.
import { type DialectProfile, expectedDialectMigrations } from "@app/config"
import { sessions } from "@conusai-kit/auth"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { orgMemberships, users } from "@conusai-kit/tenancy"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import type { DialectData } from "../../apps/worker/src/dialect/engine.js"
import { engineFor } from "../../apps/worker/src/dialect/runtime.js"
import { makeApp, type Person } from "../../modules/account/__tests__/harness.js"
import type { AccountExport, DeletionView } from "../../modules/account/index.js"
import { accountDeletions } from "../../modules/account/index.js"
import { freshDialectModules } from "./fixture.js"
const ENGINES: readonly [DialectProfile, string, string | undefined][] = [
["mariadb", "MARIADB_URL", process.env.ACCOUNT_TEST_MARIADB_URL],
["mongodb", "MONGODB_URL", process.env.ACCOUNT_TEST_MONGODB_URL],
]
/** What the migrate job of the profile applies (apps/worker/src/dialect/), on the engine under test. */
async function migrate(
profile: DialectProfile,
env: Record<string, string>,
registry: ReturnType<typeof buildRegistry>,
): Promise<string[]> {
const outcome = await engineFor(profile).migrate(env, registry)
return [...outcome.applied, ...outcome.skipped]
}
async function orgOf(person: Person, slug: string): Promise<string> {
const created = (await person.call().callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string }
return created.orgId
}
for (const [profile, variable, url] of ENGINES) {
const modules = freshDialectModules(profile)
const registry = buildRegistry(modules)
describe.skipIf(!url)(`account data-subject rights on ${profile} (${variable})`, () => {
let data: DialectData
const env = { [variable]: url ?? "" }
beforeAll(async () => {
// The migrate job's whole list applies on the engine, and it is the list readiness expects.
expect((await migrate(profile, env, registry)).sort()).toEqual(
(await expectedDialectMigrations(registry, profile)).sort(),
)
const entities = [...entitiesOf(registry, profile), ...KIT_JOBS_ENTITIES]
expect(entities.map(({ table }) => table)).toEqual(
expect.arrayContaining(["account_deletion", "account_session_revocation"]),
)
data = await engineFor(profile).connect(env, entities)
}, 120_000)
afterAll(async () => {
await data?.close()
})
it("exports the person's own rows and deletes the account, leaving another person's as it was", async () => {
const app = makeApp({ data, modules })
const tag = Math.random().toString(36).slice(2, 8)
const ada = await app.signUp(`ada-${tag}@example.test`)
const grace = await app.signUp(`grace-${tag}@example.test`)
const adaOrg = await orgOf(ada, `solo-${tag}`)
await orgOf(grace, `other-${tag}`)
const exported = (await ada.call().callByName("account.data.export", {})) as AccountExport
expect(exported).toMatchObject({ subject: ada.actor.userId, complete: true, failed: [] })
expect(exported.sections.account).toMatchObject({
status: "ok",
data: { profile: { userId: ada.actor.userId, email: ada.email }, organizations: [{ slug: `solo-${tag}` }] },
})
// Negative control: nothing of the other person is in it.
expect(JSON.stringify(exported)).not.toContain(grace.email)
expect(JSON.stringify(exported)).not.toContain(`other-${tag}`)
const view = (await ada.call().callByName("account.deletion.request", { confirm: ada.email })) as DeletionView
expect(view).toMatchObject({ state: "completed", pending: [], abandoned: [] })
expect(await app.data.global(users).get(ada.actor.userId)).toBeNull()
expect(
await app.data.forScope({ level: "org", orgId: adaOrg, actor: ada.actor }).of(orgMemberships).count(),
).toBe(0)
const adaSessions = (
await app.data.global(sessions).list({ where: [{ field: "userId", op: "eq", value: ada.actor.userId }] })
).items
expect(adaSessions).toEqual([])
const ledger = (
await app.data
.global(accountDeletions)
.list({ where: [{ field: "userId", op: "eq", value: ada.actor.userId }] })
).items
expect(ledger).toMatchObject([{ state: "completed" }])
expect(JSON.stringify(ledger)).not.toContain(ada.email)
// The other person keeps everything: their user row, their membership and their export.
expect((await app.data.global(users).get(grace.actor.userId))?.email).toBe(grace.email)
const graceExport = (await grace.call().callByName("account.data.export", {})) as AccountExport
expect(graceExport.sections.account).toMatchObject({
status: "ok",
data: { organizations: [{ slug: `other-${tag}` }] },
})
}, 60_000)
})
}
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The mariadb and mongodb compositions (profiles/<name>/README.md): the migrate job applies exactly
// the list the web shell's readiness probe expects, the job adapter's own table among them, and the
// worker serves every module job on BullMQ. `kit template:prune --profile postgres` removes this folder.
import { expectedDialectMigrations } from "@app/config"
import { appModules } from "@app/modules"
import { buildRegistry } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import { mariadbJobsMigration } from "../../apps/worker/src/dialect/mariadb/index.js"
import { mongodbJobsCommands } from "../../apps/worker/src/dialect/mongodb/index.js"
import { bullmqJobs, dialectMigrations, queueSlug } from "../../apps/worker/src/dialect/runtime.js"
import { dialectSchedules } from "../../apps/worker/src/dialect/schedules.js"
const registry = buildRegistry([...appModules])
describe("the dialect profiles' migrations", () => {
for (const profile of ["mariadb", "mongodb"] as const) {
it(`${profile}: the migrate job applies what readiness expects, kernel, jobs table and modules`, async () => {
const expected = await expectedDialectMigrations(registry, profile)
const applied = (await dialectMigrations(registry, profile)).map((migration) => migration.name)
expect([...applied].sort()).toEqual([...expected].sort())
expect(expected).toEqual(
expect.arrayContaining([
"0001_kernel",
"0007_kit-jobs",
"notes/0001_notes", // kit:notes
"account/0001_account",
]),
)
})
}
it("builds the outbox relay's ledger table in each dialect", () => {
expect(mariadbJobsMigration().statements.join("\n")).toContain("create table `kit_processed_events`")
expect(mongodbJobsCommands()).toContainEqual(expect.objectContaining({ createCollection: "kit_processed_events" }))
})
})
describe("the dialect profiles' worker", () => {
it("serves every module job of the registry, with its own payload schema", () => {
const jobs = bullmqJobs(registry)
expect(Object.keys(jobs).sort()).toEqual(Object.keys(registry.jobs).sort())
for (const [name, def] of Object.entries(jobs)) expect(def.payload).toBe(registry.jobs[name]?.def.payload)
})
it("schedules on each dialect worker only the module jobs of modules installed on its profile", () => {
for (const profile of ["mariadb", "mongodb"] as const) {
const jobs = dialectSchedules(registry, profile)
.map((schedule) => schedule.job)
.sort()
// The account, file and inbox sweeps run on both; the agent module ships Postgres only.
expect(jobs).toEqual([
"account.deletion.sweep",
"account.sessions.revoke",
"files.recover",
"notifications.retention.sweep",
])
}
})
it("keys the queue by the platform's project, in the alphabet BullMQ prefixes take", () => {
const env = (id: string) => ({ deployment: { CONUSAI_PROJECT_ID: id } }) as Parameters<typeof queueSlug>[0]
expect(queueSlug(env("42"))).toBe("p-42")
expect(queueSlug(env("Proj_ID.9"))).toMatch(/^[a-z0-9][a-z0-9-]*$/)
})
})
+10
View File
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Fresh, synthetic backend-conformance fixtures: no existing app, record or
// adoption history is rewritten. Only modules supporting the tested profile
// are installed in this new fixture's registry and its matching data source.
import type { DialectProfile } from "@app/config"
import { appModules } from "@app/modules"
export function freshDialectModules(profile: DialectProfile) {
return appModules.filter((module) => module.profiles === undefined || module.profiles.includes(profile))
}
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { type AppEnv, loadAppEnv, profileRegistry, type ServerObservability } from "@app/config"
import { appModules, moduleInclusion } from "@app/modules"
import { buildRegistry } from "@conusai-kit/core"
import { createMemoryDataSource, entitiesOf } from "@conusai-kit/data"
import { users } from "@conusai-kit/tenancy"
import { afterEach, describe, expect, it, vi } from "vitest"
import { buildDialectWorkerRuntime, engineFor, migrateDialect } from "../../apps/worker/src/dialect/runtime.js"
import { makeApp } from "../../modules/account/__tests__/harness.js"
import type { AccountExport, DeletionView } from "../../modules/account/index.js"
import { freshDialectModules } from "./fixture.js"
afterEach(() => vi.restoreAllMocks())
describe.each(["mariadb", "mongodb"] as const)("%s module/profile composition", (profile) => {
it("names unsupported adopted modules and preserves their custody registry", () => {
const before = JSON.stringify(moduleInclusion)
expect(() => profileRegistry(appModules, profile)).toThrow(/agent, assistant/)
expect(JSON.stringify(moduleInclusion)).toBe(before)
expect(buildRegistry([...appModules]).modules.map(({ id }) => id)).toContain("assistant")
})
it("uses the same compatible fresh registry for data and account rights without changing starter history", async () => {
const before = JSON.stringify(moduleInclusion)
const modules = freshDialectModules(profile)
const registry = profileRegistry(modules, profile)
const data = createMemoryDataSource({ entities: entitiesOf(registry, profile) })
const app = makeApp({ data, modules })
expect(app.registry.modules.map(({ id }) => id)).toEqual(registry.modules.map(({ id }) => id))
const ada = await app.signUp("profile-ada@example.test")
const grace = await app.signUp("profile-grace@example.test")
await ada.call().callByName("tenancy.org.create", { slug: "profile-ada", name: "Ada fixture" })
await grace.call().callByName("tenancy.org.create", { slug: "profile-grace", name: "Grace fixture" })
const exported = (await ada.call().callByName("account.data.export", {})) as AccountExport
expect(exported).toMatchObject({ subject: ada.actor.userId, complete: true, failed: [] })
expect(JSON.stringify(exported)).not.toContain(grace.email)
const deleted = (await ada.call().callByName("account.deletion.request", { confirm: ada.email })) as DeletionView
expect(deleted).toMatchObject({ state: "completed", pending: [], abandoned: [] })
expect(await data.global(users).get(ada.actor.userId)).toBeNull()
expect((await data.global(users).get(grace.actor.userId))?.email).toBe(grace.email)
expect(JSON.stringify(moduleInclusion)).toBe(before)
})
it("refuses the actual worker and migrate paths before connecting or applying migrations", async () => {
const databaseUrl = `${profile === "mariadb" ? "mysql" : "mongodb"}://127.0.0.1:1/fixture`
const raw: Record<string, string> = {
NODE_ENV: "test",
CONUSAI_API_URL: "https://fixture.example.test/api",
CONUSAI_API_TOKEN: "synthetic-token",
CONUSAI_PROJECT_ID: "profile-refusal",
REDIS_URL: "redis://127.0.0.1:1",
[profile === "mariadb" ? "MARIADB_URL" : "MONGODB_URL"]:
`${profile === "mariadb" ? "mysql" : "mongodb"}://127.0.0.1:1/fixture`,
}
if (profile === "mariadb") raw.DATABASE_URL = databaseUrl
const env: AppEnv = loadAppEnv("worker", raw, profile)
const engine = engineFor(profile)
const connect = vi
.spyOn(engine, "connect")
.mockRejectedValue(new Error("database access forbidden in refusal control"))
const migrate = vi.spyOn(engine, "migrate").mockRejectedValue(new Error("migration forbidden in refusal control"))
await expect(buildDialectWorkerRuntime(env, {} as ServerObservability)).rejects.toThrow(/agent, assistant/)
expect(() => migrateDialect(env)).toThrow(/agent, assistant/)
expect(connect).not.toHaveBeenCalled()
expect(migrate).not.toHaveBeenCalled()
})
})
+130
View File
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The web shell's stores of the mariadb and mongodb profiles (apps/web/src/server/dialect/) on the
// real engines: the readiness probes answer, the migration level the migrate job wrote is what
// readiness expects, the MongoDB store refuses a standalone server at boot (AT-D15), and the BullMQ
// queue enqueues on Valkey under the app's own prefix.
//
// It needs ACCOUNT_TEST_MARIADB_URL, ACCOUNT_TEST_MONGODB_URL (a replica set) and ACCOUNT_TEST_REDIS_URL,
// the databases test/dialect/account.test.ts migrated; without them each block is skipped by name.
import { randomUUID } from "node:crypto"
import { type AppEnv, expectedDialectMigrations, liveCaptureMigrations, loadAppEnv } from "@app/config"
import { notes } from "@app/modules/notes"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { Redis } from "ioredis"
import { describe, expect, it } from "vitest"
import { dialectStores, queueSlug } from "../../apps/web/src/server/dialect/stores"
import { engineFor } from "../../apps/worker/src/dialect/runtime.js"
import { freshDialectModules } from "./fixture.js"
const redisUrl = process.env.ACCOUNT_TEST_REDIS_URL
const platform = {
NODE_ENV: "test",
CONUSAI_API_URL: "https://api.example.test/api",
CONUSAI_API_TOKEN: "token",
CONUSAI_PROJECT_ID: "w06-stores",
}
const ENGINES = [
[
"mariadb",
{ MARIADB_URL: process.env.ACCOUNT_TEST_MARIADB_URL, DATABASE_URL: process.env.ACCOUNT_TEST_MARIADB_URL },
],
["mongodb", { MONGODB_URL: process.env.ACCOUNT_TEST_MONGODB_URL }],
] as const
for (const [profile, store] of ENGINES) {
const registry = buildRegistry(freshDialectModules(profile))
const url = Object.values(store)[0]
describe.skipIf(!url || !redisUrl)(`the web shell's ${profile} store`, () => {
it("answers readiness at the migrate job's level and enqueues on its own BullMQ prefix", async () => {
const raw = { ...platform, ...store, REDIS_URL: redisUrl }
await engineFor(profile).migrate(raw, registry)
const env: AppEnv = loadAppEnv("web", raw, profile)
const stores = dialectStores(
env,
[...entitiesOf(registry, profile), ...KIT_JOBS_ENTITIES],
liveCaptureMigrations(registry, profile),
)
if (!stores) throw new Error("no dialect store")
try {
await stores.probes.boot()
await stores.probes.store()
const applied = await stores.probes.applied()
for (const name of await expectedDialectMigrations(registry, profile))
expect(applied.has(name), name).toBe(true)
// A request's enqueue reaches Valkey under this app's prefix.
const id = await stores
.queue()
.enqueue(
{ level: "org", orgId: "00000000-0000-4000-8000-000000000001", actor: { userId: "u" } },
"files.recover",
{ probe: true },
)
expect(id).toBeTruthy()
const redis = new Redis(redisUrl as string)
try {
const keys = await redis.keys(`kit-jobs-${queueSlug(env)}*`)
expect(keys.length).toBeGreaterThan(0)
} finally {
await redis.quit()
}
// The live log: the migrate job prepared notes_note (MariaDB row triggers, MongoDB pre-images),
// so a note written through the repository is in the log, ids only, in its own organization.
{
const log = stores.liveLog?.()
if (!log) throw new Error(`the ${profile} store has no live log`)
await log.start()
const scope = {
level: "project",
orgId: randomUUID(),
workspaceId: randomUUID(),
projectId: randomUUID(),
actor: { userId: randomUUID() },
} as const
const since = profile === "mongodb" ? (await log.head(scope.orgId)).position : { txid: 0n, seq: 0n }
const note = await stores.data
.forScope(scope)
.of(notes)
.insert({ title: "live", body: "b", createdBy: scope.actor.userId })
let rows: Awaited<ReturnType<typeof log.read>>["final"] = []
for (let attempt = 0; attempt < 50 && rows.length === 0; attempt++) {
const read = await log.read(scope.orgId, since, 10)
rows = [...read.final, ...read.early]
if (rows.length === 0) await new Promise((resolve) => setTimeout(resolve, 100))
}
expect(rows.map((row) => [row.entity, row.id, row.op, row.scope.ws])).toEqual([
["notes.note", note.id, "u", scope.workspaceId],
])
expect((await log.read(randomUUID(), since, 10)).final).toEqual([])
await log.close()
}
} finally {
await stores.close()
}
}, 60_000)
})
}
describe.skipIf(!process.env.ACCOUNT_TEST_MONGODB_STANDALONE_URL)(
"the web shell's mongodb store on a standalone server",
() => {
it("refuses to boot with the typed replica_set error (AT-D15)", async () => {
const env = loadAppEnv(
"web",
{ ...platform, MONGODB_URL: process.env.ACCOUNT_TEST_MONGODB_STANDALONE_URL, REDIS_URL: "redis://127.0.0.1:1" },
"mongodb",
)
const registry = buildRegistry(freshDialectModules("mongodb"))
const stores = dialectStores(env, entitiesOf(registry, "mongodb"))
if (!stores) throw new Error("no dialect store")
try {
await expect(stores.probes.boot()).rejects.toMatchObject({ name: "MongoTopologyError", setting: "replica_set" })
} finally {
await stores.close()
}
}, 60_000)
},
)
+86
View File
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The image the three processes run from (spec §13). These are the properties a deployment
// depends on, and each one has cost a deployment somewhere: a manifest the install stage does not
// copy, a root process, a base image nobody pinned to the Node the app is written for.
import { readdirSync, readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { parse } from "yaml"
const dockerfile = readFileSync(new URL("../Dockerfile", import.meta.url), "utf8")
const workspace = parse(readFileSync(new URL("../pnpm-workspace.yaml", import.meta.url), "utf8")) as {
minimumReleaseAgeExclude: string[]
trustPolicy: string
trustPolicyExclude: string[]
}
describe("the Dockerfile", () => {
it("is multi-stage, with the runtime stage compose.yaml names", () => {
expect(dockerfile).toMatch(/^FROM node:24-slim AS deps$/m)
expect(dockerfile).toMatch(/^FROM deps AS build$/m)
expect(dockerfile).toMatch(/^FROM node:24-slim AS runtime$/m)
})
it("copies the manifest of every workspace package before installing", () => {
// The install stage resolves the whole workspace, so a package whose manifest is missing
// fails the install with "no package named @app/<id> is present in the workspace".
const packages = readdirSync(new URL("..", import.meta.url), { withFileTypes: true })
expect(packages.some((entry) => entry.name === "pnpm-workspace.yaml")).toBe(true)
for (const dir of ["config", "modules", "apps/web", "apps/worker"]) {
expect(dockerfile, `${dir}/package.json is not copied`).toContain(`COPY ${dir}/package.json ./${dir}/`)
}
})
it("keeps code root-owned but lets the runtime user write generated code", () => {
expect(dockerfile).toMatch(/^USER node$/m)
expect(dockerfile).toContain("--chown=root:root")
expect(dockerfile).toContain(
"chown -R node:node /app/convex/_generated /app/better-auth/functions/_generated /app/better-auth/functions/betterAuth/_generated",
)
})
it("takes the registry credential as a build secret, never as a layer or a build argument", () => {
// Two build paths deliver the same credential under two ids: a preset single-image build
// mounts it as `npmrc`, a compose build as `conusai_npmrc` (the platform prefixes its compose
// secrets so a tenant's own can never collide). The install must accept either — a
// deployment that built through the preset path got no credential at all and pnpm fell back
// to the public registry, where the vendor scopes 404 (stage, deployment 4, 2026-10-20).
expect(dockerfile).toContain("--mount=type=secret,id=conusai_npmrc,target=/run/secrets/conusai_npmrc")
expect(dockerfile).toContain("--mount=type=secret,id=npmrc,target=/run/secrets/npmrc")
expect(dockerfile).not.toMatch(/ARG .*(TOKEN|PASSWORD|SECRET)/)
expect(dockerfile).not.toMatch(/_authToken/)
})
it("excludes the vendor scopes from the release-age cooldown", () => {
// minimumReleaseAge blocks versions published in the last seven days — but every dev
// bundle publishes the vendor packages minutes before a deployment installs them, from
// the ConusAI vendor registry, so the cooldown must never apply to either scope
// (stage, deployment 5, 2026-10-20: ERR_PNPM_NO_MATURE_MATCHING_VERSION).
expect(workspace.minimumReleaseAgeExclude).toContain("@conusai-sdk/*")
expect(workspace.minimumReleaseAgeExclude).toContain("@conusai-kit/*")
})
it("keeps the trust policy on, excepting one exact version", () => {
// A range or a whole package here would switch the policy off for releases nobody has looked at: an entry names
// the version, its chain and its reason (pnpm-workspace.yaml), and its integrity is checked against the registry.
expect(workspace.trustPolicy).toBe("no-downgrade")
expect(workspace.trustPolicyExclude).toEqual(["semver@6.3.1"])
})
it("starts no process by default: compose.yaml names the one each service runs", () => {
const cmd = dockerfile.match(/^CMD (.*)$/m)?.[1] ?? ""
expect(cmd).toContain("compose.yaml")
expect(cmd).not.toMatch(/server\.mjs|vite|main\.js|migrate\.js/)
})
it("carries the Start build, and compose.yaml starts the web process on it with no package manager", () => {
// The web service's argv is the server itself: a package manager as PID 1 does not forward SIGTERM.
const compose = parse(readFileSync(new URL("../compose.yaml", import.meta.url), "utf8")) as {
services: Record<string, { command?: string[]; working_dir?: string }>
}
expect(compose.services.web?.working_dir).toBe("/app/apps/web")
expect(compose.services.web?.command).toEqual(["node", "server.mjs"])
expect(dockerfile).not.toContain(".next")
expect(dockerfile).not.toMatch(/\bnext (?:build|start)\b/)
})
})
+431
View File
@@ -0,0 +1,431 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The extraction of the documents module through the ModelGateway (plan item AT2.3): what the model is
// sent, what is done with its answer, and what happens when it does not answer. The model is the fake
// platform's gateway route, scripted per test; the gateway client, the extractor, the verification, the
// cache, the proposals and the review all run for real.
import { type LineView, LOW_TRUST_NOTE } from "@app/modules/documents"
import { uploadViaProcedures } from "@conusai-kit/testing"
import type { FakeChatResponder } from "@conusai-kit/testing/fake-platform"
import { afterEach, describe, expect, it } from "vitest"
import {
bytesOf,
closeApps,
type DocumentsApp,
makeDocumentsApp,
type SeededProject,
uploadAndProcess,
} from "./support/documents-app.js"
import { deterministicModel } from "./support/fake-model.js"
afterEach(closeApps)
/** Every page image the platform drew, as the fake platform recorded it. */
async function pagesDrawn(app: DocumentsApp): Promise<Array<{ job: number; page: number; dpi: number }>> {
const state = (await (await fetch(`${app.platform.url}/__e2e/state`)).json()) as {
ocrPages: Array<{ job: number; page: number; dpi: number }>
}
return state.ocrPages
}
const PAGE = { width: 1000, height: 1400, unit: "px", dpi: 150, rotation: 0 }
/**
* One page of an OCR document whose lines are `rows`, 40 px apart, one block per line (`p{n}-b{m}`), as
* the platform returns it once the page is classified: a scan when it was recognised, text otherwise.
* `repeated` lists the rows the reader flagged as repeating on most pages.
*/
function pageOf(
index: number,
rows: string[],
source: "text_layer" | "ocr" = "text_layer",
confidence = 0.97,
options: { pageClass?: string; repeated?: readonly string[] } = {},
) {
return {
...PAGE,
index,
class: options.pageClass ?? (source === "ocr" ? "scan" : "text"),
source,
text: rows.join("\n"),
lines: rows.map((text, order) => ({
text,
bbox: [100, 100 + order * 40, 600, 130 + order * 40],
polygon: null,
confidence,
order,
words: [],
repeated: options.repeated?.includes(text) ?? false,
})),
blocks: rows.map((_, order) => ({
id: `p${index + 1}-b${order + 1}`,
bbox: [90, 90 + order * 40, 700, 140 + order * 40],
order,
lines: [order],
})),
}
}
/** A row long enough that a page of it is not weak for want of text: its image is not asked for. */
const SHEET_ROW = "Reference: QR-2026-0417 (rev 2)"
const documentOf = (...pages: Array<ReturnType<typeof pageOf>>) => ({
schema_version: "conusai.ocr.document/v1",
engine: { name: "fixture-engine", version: "1", detection_model: null, recognition_model: null },
page_count: pages.length,
pages,
})
/** A one-page OCR document whose lines are `rows`. */
const onePage = (rows: string[], source: "text_layer" | "ocr" = "text_layer", confidence = 0.97) =>
documentOf(pageOf(0, rows, source, confidence))
interface Detail {
document: {
status: string
failureCode: string | null
pending: number
skippedPages: Array<{ page: number; pageClass: string }>
}
lines: LineView[]
}
async function run(
responder: FakeChatResponder | undefined,
document: Record<string, unknown>,
upload: { filename: string; contentType: string } = { filename: "report.pdf", contentType: "application/pdf" },
): Promise<{ app: DocumentsApp; project: SeededProject; detail: Detail }> {
const app = await makeDocumentsApp({ document, ...(responder ? { responder } : {}) })
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project, upload.filename, upload.contentType)
const detail = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
return { app, project, detail }
}
const reply = (lines: Array<{ label: string; value: string; quote: string }>) => ({
content: JSON.stringify({ lines }),
})
type Message = { role: string; content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> }
const messagesOf = (request: Record<string, unknown>) => request.messages as Message[]
const userParts = (request: Record<string, unknown>) => {
const content = messagesOf(request).find((message) => message.role === "user")?.content
return typeof content === "string" ? [] : (content ?? [])
}
describe("what the model is sent", () => {
it("asks once per page, with the page's text and the schema of the module's line", async () => {
const { app } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(app.model.requests).toHaveLength(1)
const request = app.model.requests[0] as Record<string, unknown>
expect(request.model).toBe("fake-model")
expect(request.temperature).toBe(0)
const [system] = messagesOf(request)
expect(system?.role).toBe("system")
const [text, ...rest] = userParts(request)
expect(text?.text).toContain("Page 1 of 1.")
expect(text?.text).toContain("Reference: QR-1\nIssued on: 2026-04-01")
// a text-layer PDF page needs no image
expect(rest).toEqual([])
const format = request.response_format as {
type: string
json_schema: { schema: { properties: { lines: { items: { required: string[]; properties: object } } } } }
}
expect(format.type).toBe("json_schema")
const item = format.json_schema.schema.properties.lines.items
expect(item.required.sort()).toEqual(["label", "quote", "value"])
// the page, the position and the confidence are the code's to set, so the model is not asked for them
expect(Object.keys(item.properties).sort()).toEqual(["label", "quote", "value"])
})
it("adds the page image, as a content part, for a scanned page of an image upload", async () => {
const { app } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7), {
filename: "scan.png",
contentType: "image/png",
})
const parts = userParts(app.model.requests[0] as Record<string, unknown>)
expect(parts.map((part) => part.type)).toEqual(["text", "image_url"])
expect(parts[1]?.image_url?.url).toMatch(/^data:image\/png;base64,/)
})
it("fetches the image of a scanned page of a PDF from the platform's page route and sends it with the text", async () => {
const { app, detail } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7))
const parts = userParts(app.model.requests[0] as Record<string, unknown>)
expect(parts.map((part) => part.type)).toEqual(["text", "image_url"])
expect(parts[0]?.text).toContain(LOW_TRUST_NOTE)
expect(parts[1]?.image_url?.url).toMatch(/^data:image\/png;base64,iVBORw0KGgo/)
// the page was drawn once, for the job that read the file, at the default dpi
expect(await pagesDrawn(app)).toEqual([{ job: 1, page: 1, dpi: 150 }])
expect(detail.lines.map((line) => line.value)).toEqual(["QR-1"])
})
it("sends a table page as image and text, and a text page as text alone without asking for its image", async () => {
const { app } = await run(
undefined,
documentOf(pageOf(0, ["Item | Qty", "Bolts | 12"], "ocr", 0.97, { pageClass: "table" }), pageOf(1, [SHEET_ROW])),
)
const kinds = app.model.requests.map((request) =>
userParts(request as Record<string, unknown>).map((part) => part.type),
)
expect(kinds).toContainEqual(["text", "image_url"])
expect(kinds).toContainEqual(["text"])
expect(await pagesDrawn(app)).toEqual([{ job: 1, page: 1, dpi: 150 }])
})
it("leaves the header that repeats on the pages out of the text the model is shown", async () => {
const header = "ACME: Specification sheet"
const { app, detail } = await run(
undefined,
documentOf(
pageOf(0, [header, "Reference: QR-1"], "text_layer", 0.97, { repeated: [header] }),
pageOf(1, [header, "Issued on: 2026-04-01"], "text_layer", 0.97, { repeated: [header] }),
),
)
for (const request of app.model.requests) {
expect(userParts(request as Record<string, unknown>)[0]?.text).not.toContain("ACME")
}
expect(detail.lines.map((line) => line.label)).toEqual(["Reference", "Issued on"])
})
it("puts the reader's block id in the evidence of a line, and its block's box", async () => {
const { project, detail } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(detail.lines.map((line) => line.blockId)).toEqual(["p1-b1", "p1-b2"])
expect(detail.lines[1]?.box).toEqual({ x: 0.09, y: 130 / 1400, width: 0.61, height: 50 / 1400 })
const proposals = (await project.call.callByName("proposals.proposal.list", {
...project.slugs,
status: "pending",
})) as { proposals: Array<{ evidence: Array<{ blockId?: string }> }> }
expect(proposals.proposals.flatMap((proposal) => proposal.evidence.map((item) => item.blockId)).sort()).toEqual([
"p1-b1",
"p1-b2",
])
})
})
describe("the pages left out", () => {
const labels = Array.from({ length: 300 }, (_, at) => `A-${at} 2400`)
it("never asks the model about a drawing or an empty page, and reports them on the document", async () => {
const { app, detail } = await run(
undefined,
documentOf(
pageOf(0, [SHEET_ROW]),
pageOf(1, labels, "text_layer", 0.97, { pageClass: "drawing" }),
pageOf(2, [], "ocr", 0.97, { pageClass: "empty" }),
),
)
expect(app.model.requests).toHaveLength(1)
expect(detail.document).toMatchObject({
status: "extracted",
skippedPages: [
{ page: 2, pageClass: "drawing" },
{ page: 3, pageClass: "empty" },
],
})
expect(detail.lines.map((line) => line.value)).toEqual(["QR-2026-0417 (rev 2)"])
// no image was drawn for a page nobody reads
expect(await pagesDrawn(app)).toEqual([])
})
it("reads a scan beside a drawing: the scan produces lines, the drawing none", async () => {
const { detail } = await run(
undefined,
documentOf(
pageOf(0, labels, "text_layer", 0.97, { pageClass: "drawing" }),
pageOf(1, ["Delivery no: DN-77", "Received by: A. Sample"], "ocr", 0.8),
),
)
expect(detail.lines.map((line) => [line.page, line.value])).toEqual([
[2, "DN-77"],
[2, "A. Sample"],
])
expect(detail.document.skippedPages).toEqual([{ page: 1, pageClass: "drawing" }])
})
it("reads a drawing when the deployment's list does not exclude it", async () => {
const app = await makeDocumentsApp({
document: documentOf(pageOf(0, ["Scale: 1:50"], "ocr", 0.9, { pageClass: "drawing" })),
excludedPageClasses: [],
})
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project, "plan.pdf", "application/pdf")
const detail = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
expect(detail.document.skippedPages).toEqual([])
expect(detail.lines.map((line) => line.value)).toEqual(["1:50"])
})
})
describe("a quote taken from the image of a scanned page", () => {
it("stays needs_review with the scanned-quote note when the page text does not hold it", async () => {
const { detail } = await run(
() => reply([{ label: "Qty", value: "12", quote: "Qty 12 pcs" }]),
onePage(["Delivery note"], "ocr", 0.7),
)
expect(detail.lines).toHaveLength(1)
expect(detail.lines[0]).toMatchObject({ verification: "needs_review", note: "scanned_quote", confidence: 0 })
})
it("carries no note for a quote the text holds", async () => {
const { detail } = await run(undefined, onePage(["Reference: QR-1"], "ocr", 0.7))
expect(detail.lines[0]).toMatchObject({ verification: "verified", note: null, blockId: "p1-b1" })
})
})
describe("what is done with the answer", () => {
const rows = ["Sąskaita: INV-2026-07", "Suma: 1 250,00 EUR", "Pastaba: apmokėti per 14 dienų"]
it("verifies each quote on the page it was asked about, folding only whitespace, case and diacritics", async () => {
const { detail } = await run(
() =>
reply([
{ label: "Sąskaita", value: "INV-2026-07", quote: "Sąskaita: INV-2026-07" },
// case, diacritics and whitespace differ from the page: still the same words
{ label: "Suma", value: "1 250,00 EUR", quote: "suma: 1 250,00\n eur" },
{ label: "Pastaba", value: "apmokėti per 14 dienų", quote: "PASTABA: APMOKETI PER 14 DIENU" },
]),
onePage(rows),
)
expect(detail.lines.map((line) => [line.label, line.verification])).toEqual([
["Sąskaita", "verified"],
["Suma", "verified"],
["Pastaba", "verified"],
])
// a verified line is located from the reader's own lines, and carries the reader's confidence
expect(detail.lines[0]?.box).not.toBeNull()
expect(detail.lines[0]?.confidence).toBe(0.97)
expect(detail.lines.every((line) => line.page === 1 && line.status === "pending")).toBe(true)
})
it("keeps a line whose quote is not on the page as needs_review, with no box and no confidence", async () => {
const { detail } = await run(
() =>
reply([
{ label: "Sąskaita", value: "INV-2026-07", quote: "Sąskaita: INV-2026-07" },
// a changed digit is a different word: folding does not forgive it
{ label: "Suma", value: "1 520,00 EUR", quote: "Suma: 1 520,00 EUR" },
{ label: "Made up", value: "x", quote: "Nothing like this is written here" },
]),
onePage(rows),
)
expect(detail.lines.map((line) => [line.label, line.verification])).toEqual([
["Sąskaita", "verified"],
["Suma", "needs_review"],
["Made up", "needs_review"],
])
const flagged = detail.lines.filter((line) => line.verification === "needs_review")
expect(flagged.every((line) => line.box === null && line.confidence === 0 && line.status === "pending")).toBe(true)
// kept, not dropped and not approved: the document is waiting for a person
expect(detail.document).toMatchObject({ status: "extracted", pending: 3 })
})
it("verifies against the page the model was asked about, not against another page", async () => {
const two = {
...onePage(["Reference: QR-1"]),
page_count: 2,
pages: [...onePage(["Reference: QR-1"]).pages, { ...onePage(["Total: 5 EUR"]).pages[0], index: 1 }],
}
// the model answers every page with page 1's line: on page 2 it is not there
const { detail } = await run(() => reply([{ label: "Reference", value: "QR-1", quote: "Reference: QR-1" }]), two)
expect(detail.lines.map((line) => [line.page, line.verification])).toEqual([
[1, "verified"],
[2, "needs_review"],
])
})
it("drops a line the model repeats", async () => {
const line = { label: "Reference", value: "QR-1", quote: "Reference: QR-1" }
const { detail } = await run(() => reply([line, line]), onePage(["Reference: QR-1"]))
expect(detail.lines).toHaveLength(1)
})
})
describe("when the model does not answer", () => {
const document = onePage(["Reference: QR-1"])
it("a refusal is asked again once, then ends the document with a typed code and no proposals", async () => {
const { app, detail } = await run(() => ({ content: null, refusal: "I cannot help with that." }), document)
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_refused" })
expect(detail.lines).toEqual([])
})
it("a reply cut off at the token ceiling is asked again with more room, and the second answer is used", async () => {
let calls = 0
const { app, detail } = await run((request) => {
calls += 1
return calls === 1 ? { content: '{"lines": [{"label"', finishReason: "length" } : deterministicModel(request)
}, document)
expect(app.model.requests.map((request) => request.max_tokens)).toEqual([4096, 8192])
expect(detail.document.status).toBe("extracted")
expect(detail.lines).toHaveLength(1)
})
it("a reply that is always cut off ends the document as truncated", async () => {
const { app, detail } = await run(() => ({ content: '{"lines": [', finishReason: "length" }), document)
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_truncated" })
})
it("a reply outside the schema is asked again once, by the gateway, and then ends the document as invalid", async () => {
const { app, detail } = await run(() => ({ content: JSON.stringify({ lines: "none" }) }), document)
// two asks in all: the gateway's retry with the validation errors is the one retry
expect(app.model.requests).toHaveLength(2)
expect(detail.document).toMatchObject({ status: "failed", failureCode: "extraction_invalid" })
const retry = messagesOf(app.model.requests[1] as Record<string, unknown>).at(-1)
expect(retry?.content).toContain("did not match the required schema")
})
it("records no document text in the failure: the code is all the document keeps", async () => {
const { detail } = await run(() => ({ content: null, refusal: "Reference: QR-1" }), document)
expect(JSON.stringify(detail.document)).not.toContain("QR-1")
})
})
describe("the cache", () => {
it("cross-file OCR reuse requires verified engine identity while identical model inputs remain cached", async () => {
const { app, project } = await run(undefined, onePage(["Reference: QR-1", "Issued on: 2026-04-01"]))
expect(app.ocr.submitted).toHaveLength(1)
expect(app.model.requests).toHaveLength(1)
const stored = app.store.keysOf({ level: "project", orgId: project.orgId } as never)
// the OCR result, the job it came from (so its pages can still be drawn), and the page's model reply
expect(stored.filter((key) => key.startsWith("documents/cache/"))).toHaveLength(3)
// the same bytes under another name: a second file, the same content hash
const second = await uploadViaProcedures(project.call, project.slugs, {
filename: "copy.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 fixture report.pdf"),
})
await app.deliver()
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(2)
expect(app.model.requests).toHaveLength(1)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: second.fileId,
})) as Detail
expect(view.lines.map((line) => line.value)).toEqual(["QR-1", "2026-04-01"])
})
it("a change of the page's text is a different request", async () => {
const { app, project } = await run(undefined, onePage(["Reference: QR-1"]))
app.platform.ocr.setDocument(onePage(["Reference: QR-2"]))
await uploadViaProcedures(project.call, project.slugs, {
filename: "other.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 something else"),
})
await app.deliver()
await app.runJobs()
expect(app.model.requests).toHaveLength(2)
})
})
+530
View File
@@ -0,0 +1,530 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The document-review flow end to end on the fake platform (plan item AT2.3): an uploaded file is
// read by an OCR job, becomes proposals, a person approves, edits or rejects each line, and only
// approved lines are exported. Every step runs through the kernel's caller, the files module's
// processor point, the proposals module's writer and the outbox relay's behaviour, so what is proven
// is the wiring and not a unit.
import { readFileSync } from "node:fs"
import { type LineView, lines as linesTable, REVIEW_PROCEDURES, type ReadDocument } from "@app/modules/documents"
import { AppError } from "@conusai-kit/core"
import { orgMemberships } from "@conusai-kit/tenancy"
import { uploadViaProcedures } from "@conusai-kit/testing"
import { afterEach, describe, expect, it } from "vitest"
import { sheetRows } from "../modules/documents/__tests__/support/unzip.js"
import {
bytesOf,
closeApps,
type DocumentsApp,
makeDocumentsApp,
type SeededProject,
uploadAndProcess,
} from "./support/documents-app.js"
const document = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as ReadDocument & Record<string, unknown>
afterEach(closeApps)
interface Detail {
document: {
documentId: string | null
status: string
failureCode: string | null
pending: number
approved: number
pageCount: number | null
skippedPages: unknown[]
}
lines: LineView[]
}
async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> {
const app = await makeDocumentsApp({ document })
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
const detail = async (project: SeededProject, fileId: string): Promise<Detail> =>
(await project.call.callByName("documents.document.get", { ...project.slugs, fileId })) as Detail
const decide = (project: SeededProject, procedure: string, input: Record<string, unknown>) =>
project.call.callByName(procedure, { ...project.slugs, ...input })
const exportRows = async (project: SeededProject, fileId: string) => {
const made = (await decide(project, "documents.export.create", { fileId })) as {
body: string
lineCount: number
filename: string
}
return { ...made, rows: sheetRows(Buffer.from(made.body, "base64")) }
}
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
describe("uploading a document", () => {
it("answers before OCR runs: the request enqueues, and the worker reads", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 fixture"),
})
// The upload has returned and the reader has not been asked anything.
expect(app.ocr.submitted).toHaveLength(0)
expect(app.queue.of("documents.ocr")).toHaveLength(0)
// The stored file's event is what enqueues the job, once.
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(1)
expect(app.ocr.submitted).toHaveLength(0)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
const inbox = (await project.call.callByName("notifications.inbox.list", { org: project.orgSlug })) as {
items: Array<{ code: string; href: string | null }>
}
expect(inbox.items.filter((item) => item.code === "job_completed")).toHaveLength(1)
expect(inbox.items.find((item) => item.code === "job_completed")?.href).toBe(
`/${project.orgSlug}/${project.workspaceSlug}/${project.projectSlug}/documents/${file.fileId}`,
)
})
it("is not read by this module when it is not a document type it reads", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "notes.csv",
contentType: "text/csv",
bytes: bytesOf("a,b"),
})
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(0)
})
it("lists an upload nobody has read yet as queued", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
const { documents } = (await decide(project, "documents.document.list", {})) as {
documents: Array<{ fileId: string; status: string }>
}
expect(documents).toEqual([expect.objectContaining({ documentId: null, fileId: file.fileId, status: "queued" })])
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the live collection's read-back", () => {
it("reads a document again by the id of its row, with its counts, as the list shows it", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const { documents } = (await decide(project, "documents.document.list", {})) as {
documents: Array<{ documentId: string | null; fileId: string }>
}
const listed = documents.find((entry) => entry.fileId === file.fileId)
expect(listed?.documentId).toEqual(expect.any(String))
const { items } = (await decide(project, "documents.document.getMany", { ids: [listed?.documentId] })) as {
items: unknown[]
}
expect(items).toEqual([listed])
})
it("answers nothing for an id of another project or of nothing", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const documentId = (await detail(project, file.fileId)).document.documentId
const other = await app.seedProject("bravo", "owner@bravo.test")
const ask = (asker: SeededProject, ids: string[]) =>
asker.call.callByName("documents.document.getMany", { ...asker.slugs, ids }) as Promise<{ items: unknown[] }>
expect((await ask(other, [documentId as string])).items).toEqual([])
expect((await ask(project, ["00000000-0000-0000-0000-000000000000"])).items).toEqual([])
})
})
describe("the extraction", () => {
it("turns the fixture document into one pending line per labelled line, each with its page and verbatim quote", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
// the fixture's third page is a drawing: left out, and said so on the document
expect(view).toMatchObject({
status: "extracted",
pending: 4,
approved: 0,
pageCount: 3,
skippedPages: [{ page: 3, pageClass: "drawing" }],
})
expect(lines.map((line) => [line.page, line.label, line.value, line.status])).toEqual([
[1, "Reference", "QR-2026-04", "pending"],
[1, "Issued on", "2026-04-01", "pending"],
[2, "Total amount", "1 250,00 EUR", "pending"],
[2, "Signed by", "A. Example", "pending"],
])
for (const line of lines) {
expect(document.pages[line.page - 1]?.text).toContain(line.quote)
expect(line.box).not.toBeNull()
// The line says which engine and extractor produced it: the Art. 50 disclosure's subject.
expect(line.model).toBe("fake-model")
expect(line.source).toMatchObject({
ocrEngine: { name: "fixture-engine", version: document.engine.version },
model: "fake-model",
fileSha256: expect.stringMatching(/^[0-9a-f]{64}$/),
})
expect(line.promptVersion).toBe("extract-lines.v2")
}
})
it("submits the document once, with a key that names the file and its content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.submitted[0]?.idempotencyKey).toMatch(
new RegExp(`^documents\\.ocr\\.${file.fileId}\\.[0-9a-f]{64}$`),
)
})
it("writes nothing the reviewer has not approved", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(
await app.data
.forScope({ level: "project", actor: project.owner, ...scopeIds(project) })
.of(linesTable)
.count(),
).toBe(0)
// and so an export has nothing to say yet
const error = await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))
expect(error.status).toBe(409)
})
})
describe("retrying the job", () => {
it("a job run twice adds no proposal and submits no second document", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
expect(job).toBeDefined()
// The queue delivers the same job again (a lapsed claim): run its handler once more.
const again = app.queue.jobs.length
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never)
expect(app.queue.jobs).toHaveLength(again + 1)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect((await detail(project, file.fileId)).lines).toHaveLength(4)
})
it("a transient failure while waiting resumes the stored OCR job instead of submitting another", async () => {
const { app, project } = await fresh()
app.ocr.failWaitOnce(new Error("the platform is unreachable"))
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
await app.deliver()
await expect(app.runJobs()).rejects.toThrow("unreachable")
expect((await detail(project, file.fileId)).document.status).toBe("processing")
// pg-boss retries a job that throws: the same payload, the same scope.
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never, { key: "retry" })
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.waited).toHaveLength(2)
expect((await detail(project, file.fileId)).document).toMatchObject({ status: "extracted", pending: 4 })
})
it("marks the document failed, with a code and no proposals, when the platform says the job failed", async () => {
const { app, project } = await fresh()
app.platform.ocr.failJobs(true)
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
expect(view).toMatchObject({ status: "failed", failureCode: "ocr_failed" })
expect(lines).toEqual([])
})
it("marks the document failed when the stored bytes are not the ones that were uploaded", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 original"),
})
for (const [key, object] of app.store.objects)
app.store.objects.set(key, { ...object, bytes: new TextEncoder().encode("tampered") })
await app.deliver()
await app.runJobs()
expect((await detail(project, file.fileId)).document).toMatchObject({
status: "failed",
failureCode: "file_changed",
})
const inbox = (await project.call.callByName("notifications.inbox.list", { org: project.orgSlug })) as {
items: Array<{ code: string }>
}
expect(inbox.items.filter((item) => item.code === "job_failed")).toHaveLength(1)
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the review", () => {
it("approving a line writes the record and one audit event; the other lines stay pending", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.document).toMatchObject({ pending: 3, approved: 1 })
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("approved")
const approvals = app.audit.events.filter((event) => event.action === "proposals.proposal_approved")
expect(approvals).toHaveLength(1)
// ids and hashes only: no label, value or quote in an audit event
expect(JSON.stringify(approvals[0])).not.toContain("QR-2026-04")
})
it("rejecting a line changes no record, and the line stays rejected with its audit event", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId, reason: "not a field" })
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("rejected")
expect(after.document).toMatchObject({ pending: 3, approved: 0 })
expect(app.audit.events.filter((event) => event.action === "proposals.proposal_rejected")).toHaveLength(1)
// nothing was written to the approved table: the export has nothing to say
expect((await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))).status).toBe(409)
})
it("an edit replaces the line with the reviewer's words, which are then approved like any other", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [, second] = (await detail(project, file.fileId)).lines
const edited = (await decide(project, "documents.line.edit", {
lineId: second?.lineId,
expectedVersion: second?.expectedVersion,
label: "Issue date",
value: "2026-04-02",
})) as LineView
// the same line, with the reviewer's words in it
expect(edited).toMatchObject({
lineId: second?.lineId,
expectedVersion: (second?.expectedVersion ?? 0) + 1,
label: "Issue date",
value: "2026-04-02",
edited: true,
status: "pending",
})
// the quote still names the words the machine read, not the reviewer's
expect(edited.quote).toBe(second?.quote)
const stale = await refusal(
decide(project, "documents.line.edit", {
lineId: second?.lineId,
expectedVersion: second?.expectedVersion,
label: "Stale correction",
value: "outdated",
}),
)
expect(stale.status).toBe(409)
const listed = (await detail(project, file.fileId)).lines
expect(listed).toHaveLength(4)
expect(listed.find((line) => line.lineId === second?.lineId)).toMatchObject({ label: "Issue date", edited: true })
await app.deliver()
// one audit event for the edit, with no word of it
const audited = app.audit.events.filter((event) => event.action === "proposals.proposal_edited")
expect(audited).toHaveLength(1)
expect(JSON.stringify(audited[0])).not.toContain("Issue date")
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: edited.lineId,
expectedVersion: edited.expectedVersion,
})
const { rows } = await exportRows(project, file.fileId)
expect(rows).toEqual([
["Label", "Value", "Page", "Source quote", "Edited by a reviewer"],
["Issue date", "2026-04-02", 1, "Issued on: 2026-04-01", "yes"],
])
})
it("a line that has been decided cannot be edited", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId })
const error = await refusal(
decide(project, "documents.line.edit", {
lineId: first?.lineId,
expectedVersion: first?.expectedVersion,
label: "x",
value: "y",
}),
)
expect(error.status).toBe(409)
})
it("approves every pending line in one decision", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const lines = (await detail(project, file.fileId)).lines
const ids = lines.map((line) => line.lineId)
const expectations = Object.fromEntries(
ids.map((id, index) => [id, { expectedVersion: lines[index]?.expectedVersion }]),
)
await decide(project, REVIEW_PROCEDURES.approveMany, { proposalIds: ids, expectations })
expect((await detail(project, file.fileId)).document).toMatchObject({ pending: 0, approved: 4 })
})
})
describe("the export", () => {
it("contains the approved lines and only those, as an Excel workbook", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project, "quarterly.report.pdf")
const lines = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[2]?.lineId,
expectedVersion: lines[2]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[0]?.lineId,
expectedVersion: lines[0]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: lines[1]?.lineId })
const made = await exportRows(project, file.fileId)
expect(made.filename).toBe("quarterly.report-approved.xlsx")
expect(made.lineCount).toBe(2)
expect(made.rows.slice(1)).toEqual([
["Reference", "QR-2026-04", 1, "Reference: QR-2026-04", "no"],
["Total amount", "1 250,00 EUR", 2, "Total amount: 1 250,00 EUR", "no"],
])
})
it("writes one audit event naming no content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await decide(project, "documents.export.create", { fileId: file.fileId })
// The transport records a procedure's declared audit event (apps/web router); here the
// contract's own declaration is the proof, since no transport runs.
expect(app.registry.procedures.get("documents.export.create")?.def.audit).toBe("documents.export_created")
expect(app.registry.procedures.get("documents.line.edit")?.def.audit).toBe("documents.line_edited")
})
})
describe("who may do what", () => {
it("lets a viewer read a document and refuses them (404, as a stranger) an edit, an approval and an export", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const read = (await viewer.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
expect(read.lines).toHaveLength(4)
for (const [name, input] of [
[
"documents.line.edit",
{ lineId: first?.lineId, expectedVersion: first?.expectedVersion, label: "x", value: "y" },
],
[REVIEW_PROCEDURES.approve, { proposalId: first?.lineId, expectedVersion: first?.expectedVersion }],
["documents.export.create", { fileId: file.fileId }],
] as const) {
const error = await refusal(viewer.call.callByName(name, { ...project.slugs, ...input }))
// The kernel answers a member who lacks the permission exactly as it answers a stranger
// (spec §3.3): 404, so a refusal does not say which procedures exist for whom.
expect(error.status, name).toBe(404)
}
expect((await detail(project, file.fileId)).document.approved).toBe(0)
})
it("lets a member review: the permission of the line's writer is the member role's", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const member = await app.addMember(project, "member@acme.test", "member")
await member.call.callByName(REVIEW_PROCEDURES.approve, {
...project.slugs,
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
expect((await detail(project, file.fileId)).document.approved).toBe(1)
})
})
const scopeIds = (project: SeededProject) => ({
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
})
describe("current worker authority before provider costs", () => {
it("keeps default missing native evidence off and persists an honest final outcome", async () => {
const app = await makeDocumentsApp({ document, nativeAdmission: false })
const project = await app.seedProject("off", "off@example.test")
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "off.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 synthetic"),
})
await app.deliver()
await app.runJobs({ finalAttempt: true })
expect(app.ocr.submitted).toHaveLength(0)
expect(app.model.requests).toHaveLength(0)
expect((await detail(project, file.fileId)).document).toMatchObject({
status: "failed",
failureCode: "processing_exhausted",
})
})
it("rechecks the original uploader's current role instead of forging a human actor", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "revoked.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 synthetic"),
})
await app.deliver()
const repo = app.data.forScope({ level: "org", orgId: project.orgId, actor: project.owner }).of(orgMemberships)
const member = (await repo.list({ limit: 1 })).items[0]
if (!member) throw new Error("the uploader fixture must have an organization membership")
await repo.update(member.id, { role: "viewer" }, member.version)
await expect(app.runJobs()).rejects.toMatchObject({ status: 403 })
expect(app.ocr.submitted).toHaveLength(0)
expect(app.model.requests).toHaveLength(0)
})
it("a lost claim never makes a provider call or marks a permanent failure", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "lease.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 synthetic"),
})
await app.deliver()
const abort = new AbortController()
abort.abort()
await expect(app.runJobs({ finalAttempt: true, signal: abort.signal })).rejects.toBeDefined()
expect(app.ocr.submitted).toHaveLength(0)
expect(app.model.requests).toHaveLength(0)
expect((await detail(project, file.fileId)).document.status).not.toBe("failed")
})
})
+145
View File
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OCR adapter against the fake platform of @conusai-kit/testing, through the real
// `@conusai-sdk/ocr` client (kit-platform's `DocumentReader`): the job is submitted, polled and read
// over HTTP exactly as the platform's async job API does it, so what is proven here is the adapter's
// calls and not a stub of them.
import { readFileSync } from "node:fs"
import { OcrJobFailed, sdkDocumentOcr } from "@app/modules/documents"
import { OcrError } from "@conusai-sdk/ocr"
import { afterEach, describe, expect, it } from "vitest"
import { closeApps, makeDocumentsApp, uploadAndProcess } from "./support/documents-app.js"
import { startOcr } from "./support/ocr.js"
const fixture = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as Record<string, unknown>
const open: Array<{ close(): Promise<void> }> = []
afterEach(async () => {
await closeApps()
await Promise.all(open.splice(0).map((platform) => platform.close()))
})
async function ocr() {
const started = await startOcr(fixture)
open.push(started.platform)
return started
}
const stateOf = async (platform: { url: string }) =>
(await (await fetch(`${platform.url}/__e2e/state`)).json()) as { ocr: unknown[] }
describe("the OCR adapter on the fake platform", () => {
it("submits a document, polls its job and returns the document the platform read", async () => {
const { ocr: port } = await ocr()
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-1",
})
const read = await port.wait(jobId)
expect(read.engine.name).toBe("fixture-engine")
expect(read.pages).toHaveLength(3)
})
it("answers a repeated idempotency key with the same job, so a retried job pays once", async () => {
const { ocr: port, platform } = await ocr()
const bytes = new TextEncoder().encode("%PDF-1.4 same")
const first = await port.submit(bytes, { filename: "a.pdf", idempotencyKey: "k-same" })
const second = await port.submit(bytes, { filename: "a.pdf", idempotencyKey: "k-same" })
expect(second.jobId).toBe(first.jobId)
expect((await stateOf(platform)).ocr).toHaveLength(1)
})
it("reads a job the platform failed as permanent", async () => {
const { ocr: port, platform } = await ocr()
platform.ocr.failJobs(true)
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-f",
})
await expect(port.wait(jobId)).rejects.toBeInstanceOf(OcrJobFailed)
})
it("draws a page of a finished job through the platform's page route: bytes, a media type, the asked dpi", async () => {
const { ocr: port, platform } = await ocr()
platform.ocr.setPageImage(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 7]))
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-p",
})
await port.wait(jobId)
const image = await port.page(jobId, 2)
expect(image?.mediaType).toBe("image/png")
expect([...(image?.bytes ?? [])]).toEqual([0x89, 0x50, 0x4e, 0x47, 7])
const state = (await (await fetch(`${platform.url}/__e2e/state`)).json()) as {
ocrPages: Array<{ job: number; page: number; dpi: number }>
}
expect(state.ocrPages).toEqual([{ job: jobId, page: 2, dpi: 150 }])
})
it("reads a page the platform has no image of (a job it no longer holds, a page past the end) as no image", async () => {
const { ocr: port } = await ocr()
expect(await port.page(999, 1)).toBeUndefined()
const { jobId } = await port.submit(new TextEncoder().encode("%PDF-1.4"), {
filename: "a.pdf",
idempotencyKey: "k-q",
})
await port.wait(jobId)
// the fixture has three pages
expect(await port.page(jobId, 4)).toBeUndefined()
})
it("reads a failure of the page route that is not the page's as retryable", async () => {
const down = new OcrError("OCR request failed: network", { type: "network" })
const adapter = sdkDocumentOcr({
submit: async () => ({ id: 1 }) as never,
wait: async () => ({}) as never,
page: async () => Promise.reject(down),
})
await expect(adapter.page(1, 1)).rejects.toBe(down)
})
it("reads anything else as retryable: the error is thrown on, unchanged", async () => {
const transient = new OcrError("OCR request failed: network", { type: "network" })
const adapter = sdkDocumentOcr({
submit: async () => ({ id: 1 }) as never,
wait: async () => Promise.reject(transient),
page: async () => ({ bytes: new Uint8Array(), contentType: "image/png" }),
})
await expect(adapter.wait(1)).rejects.toBe(transient)
})
it("carries a whole upload through the worker over HTTP: the fixture becomes proposals", async () => {
const app = await makeDocumentsApp({ document: fixture })
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as {
document: { status: string; pending: number }
}
expect(view.document).toMatchObject({ status: "extracted", pending: 4 })
expect((await stateOf(app.platform)).ocr).toHaveLength(1)
})
it("marks a document extracted with nothing to review when the platform reads a page with no lines", async () => {
const app = await makeDocumentsApp({
document: {
...fixture,
page_count: 1,
pages: [{ index: 0, width: 10, height: 10, class: "empty", text: "", lines: [], blocks: [] }],
},
})
const project = await app.seedProject("acme", "owner@acme.test")
const file = await uploadAndProcess(app, project)
const view = (await project.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as {
document: { status: string; pending: number }
}
expect(view.document).toMatchObject({ status: "extracted", pending: 0 })
})
})
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `.env.example` is the complete, documented list of the variables this app reads (plan item AT4.5):
// every variable the code reads is in it, every variable in it is read by something, and each has a
// comment above it saying what it is for.
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
const root = new URL("..", import.meta.url).pathname
const example = readFileSync(join(root, ".env.example"), "utf8")
const declared = [...example.matchAll(/^([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1] as string)
const walk = (dir: string): string[] =>
readdirSync(dir).flatMap((name) => {
if (["node_modules", "dist", ".tanstack", ".logs", "__tests__"].includes(name)) return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path)
return /\.(ts|tsx|mjs)$/.test(name) ? [path] : []
})
/** The variables the code of this repository reads: `process.env.X`, `env.X` and the loaders' quoted names. */
function read(): Set<string> {
const found = new Set<string>()
const files = [
...walk(join(root, "apps")),
...walk(join(root, "config", "src")),
...walk(join(root, "scripts")),
...walk(join(root, "e2e", "support")),
join(root, "better-auth", "deploy.mjs"), // kit:convex-better-auth
]
for (const file of files) {
const text = readFileSync(file, "utf8")
for (const match of text.matchAll(/\b(?:process\.env|env)\.([A-Z][A-Z0-9_]+)/g)) found.add(match[1] as string)
for (const match of text.matchAll(/\b(?:process\.env|env)\[\s*"([A-Z][A-Z0-9_]+)"\s*\]/g))
found.add(match[1] as string)
}
// The names config/src/env.ts holds in constants.
for (const match of readFileSync(join(root, "config/src/env.ts"), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) {
found.add(match[1] as string)
}
for (const match of readFileSync(join(root, "config/src/documents.ts"), "utf8").matchAll(
/_VAR = "([A-Z][A-Z0-9_]+)"/g,
)) {
found.add(match[1] as string)
}
// The platform contract and the sign-in variables are read by the kit's loaders from the same environment.
const kit = (path: string) => readFileSync(join(root, "node_modules", "@conusai-kit", path), "utf8")
for (const file of ["host/dist/postgres/config/env.js", "host/dist/postgres/config/settings.js"]) {
for (const match of kit(file).matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) found.add(match[1] as string)
}
for (const match of kit("auth/dist/config.js").matchAll(/\b((?:AUTH|OIDC)_[A-Z_]+)\b/g)) found.add(match[1] as string)
for (const name of [
"CONUSAI_API_URL",
"CONUSAI_API_TOKEN",
"CONUSAI_PROJECT_ID",
"CONUSAI_ENVIRONMENT_ID",
"CONUSAI_PLATFORM_RELEASE_DESCRIPTOR",
"CONUSAI_PLATFORM_SOURCE_COMMIT",
"CONUSAI_ORG_ID",
"CONUSAI_ORG_SLUG",
"CONUSAI_WORKSPACE_SLUG",
"CONUSAI_PROJECT_SLUG",
"CONUSAI_PUBLIC_URL",
"CONUSAI_RELEASE",
"CONUSAI_ENVIRONMENT",
"CONUSAI_DSN",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"DEV_WEB_PORT",
"DEV_PLATFORM_PORT",
"DEV_POSTGRES_PORT",
"CONVEX_SELF_HOSTED_URL",
"CONVEX_SELF_HOSTED_ADMIN_KEY",
"CONVEX_BACKUP_INTERVAL_HOURS",
"CONVEX_BACKUP_RETENTION",
"CONVEX_CLI",
"CONVEX_APP_DIR",
]) {
found.add(name)
}
return found
}
describe(".env.example", () => {
it("lists every variable the code reads", () => {
const missing = [...read()].filter((name) => !declared.includes(name)).sort()
// Set by the runtime and not by a person: NODE_ENV is in the file for what it means here, and the
// framework's own variable names are not this app's to document.
const framework = new Set(["npm_execpath", "PORT", "HOST", "HOSTNAME", "E2E_BASE_URL", "E2E_PLATFORM_URL"])
expect(
missing.filter((name) => !framework.has(name)),
"add these to .env.example, with a comment",
).toEqual([])
})
it("lists nothing that nothing reads", () => {
const used = read()
const fixed = new Set([
"POSTGRES_URL",
"POSTGRES_READER_URL",
"POSTGRES_MIGRATE_URL",
"AI_MODEL",
"LOG_LEVEL",
"WORKER_HEARTBEAT_FILE",
"NODE_ENV",
])
const unused = declared.filter((name) => !used.has(name) && !fixed.has(name))
expect(unused, "remove these from .env.example, or the code that reads them is not found by this test").toEqual([])
})
it("has no variable twice", () => {
expect(declared.filter((name, index) => declared.indexOf(name) !== index)).toEqual([])
})
it("explains every variable: a comment sits above it, or above the block of variables it belongs to", () => {
const lines = example.split("\n")
const undocumented: string[] = []
for (const [index, line] of lines.entries()) {
const name = /^([A-Z][A-Z0-9_]+)=/.exec(line)?.[1]
if (!name) continue
let above = index - 1
while (above >= 0 && /^[A-Z][A-Z0-9_]+=/.test(lines[above] as string)) above -= 1
if (!(lines[above] ?? "").startsWith("#")) undocumented.push(name)
}
expect(undocumented).toEqual([])
})
it("holds no real secret: the values are the development ones or empty", () => {
for (const name of ["CONUSAI_API_TOKEN", "AUTH_SESSION_SECRET", "OIDC_WEB_CLIENT_SECRET", "BETTER_AUTH_SECRET"]) {
const value = new RegExp(`^${name}=(.*)$`, "m").exec(example)?.[1] ?? ""
expect(value === "" || /dev|replace-me/.test(value), `${name} looks like a real secret`).toBe(true)
}
})
})
+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Every place outside the tests that stands up an app's fake platform either serves the app's
// deployment (`deployment:` -- its business gateway, captured e-mail and notifications, as `pnpm dev`
// does) or says, in a marker comment just above the call, why its app needs none. A new stack that
// forgets it fails here by name instead of refusing every business procedure (TEMPLATE-LESSON proc-5).
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join, relative } from "node:path"
import { describe, expect, it } from "vitest"
const ROOT = new URL("..", import.meta.url).pathname
const MARKER = "// fake-platform: no deployment:"
const SKIP = new Set(["node_modules", "dist", "test", "__tests__", ".logs", ".dev", ".git"])
function sources(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
if (SKIP.has(name)) return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return sources(path)
return /\.(ts|tsx|mjs|js)$/.test(name) && !/\.test\./.test(name) ? [path] : []
})
}
/** The text of each `startFakePlatform(…)` call in a file, with the lines just above it. */
function calls(text: string): Array<{ line: number; call: string; above: string }> {
const found: Array<{ line: number; call: string; above: string }> = []
for (let at = text.indexOf("startFakePlatform("); at >= 0; at = text.indexOf("startFakePlatform(", at + 1)) {
let depth = 0
let end = at + "startFakePlatform".length
for (; end < text.length; end++) {
if (text[end] === "(") depth++
if (text[end] === ")" && --depth === 0) break
}
const line = text.slice(0, at).split("\n").length
found.push({
line,
call: text.slice(at, end + 1),
above: text
.split("\n")
.slice(Math.max(0, line - 4), line - 1)
.join("\n"),
})
}
return found
}
describe("every fake platform the template stands up", () => {
const sites = sources(ROOT).flatMap((path) =>
calls(readFileSync(path, "utf8")).map((site) => ({ ...site, file: relative(ROOT, path) })),
)
it("finds the stacks it holds to the rule", () => {
expect(sites.map((site) => site.file).sort()).toEqual(
expect.arrayContaining(["e2e/support/stack.ts", "scripts/dev.mjs"]),
)
})
it("serves the app's deployment, or says why its app needs none", () => {
const missing = sites
.filter((site) => !/\bdeployment\s*:/.test(site.call) && !site.above.includes(MARKER))
.map((site) => `${site.file}:${site.line}`)
expect(missing, `pass \`deployment\` to startFakePlatform, or state why not with "${MARKER} <reason>"`).toEqual([])
})
})
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The tab icon is the ConusAI mark, the file @conusai-kit/brand ships. Not a copy: the web build emits the kit's bytes
// at /favicon.png, and the document head points there.
import { existsSync, readFileSync } from "node:fs"
import { createRequire } from "node:module"
import { appBrand } from "@app/config"
import { describe, expect, it } from "vitest"
const web = new URL("../apps/web/", import.meta.url)
describe("the favicon", () => {
it("is the path the build emits", () => {
expect(appBrand.logo.favicon).toBe("/favicon.png")
})
it("is emitted from the kit's file, and the template holds no icon of its own", () => {
const config = readFileSync(new URL("vite.config.ts", web), "utf8")
expect(config).toContain(`"@conusai-kit/brand/logo/favicon.png"`)
expect(existsSync(new URL("public/favicon.svg", web))).toBe(false)
})
it("resolves to a PNG in the kit", () => {
const file = createRequire(new URL("package.json", web)).resolve("@conusai-kit/brand/logo/favicon.png")
expect(readFileSync(file).subarray(1, 4).toString()).toBe("PNG")
})
})
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Synthetic admission for injected adapter-port tests only. The fake HTTP service
// still exercises response mapping; this observer proves no installed/live state.
import type { ServiceReadiness } from "@conusai-kit/core"
export const simulatedBusinessAdmission: ServiceReadiness = {
profile: "postgres",
observe: async () => ({
desiredGeneration: 1,
observedGeneration: 1,
observedAt: Date.now(),
installed: true,
enabled: true,
entitled: true,
authorized: true,
configured: true,
compatible: true,
dependenciesReady: true,
partitionBound: true,
ready: true,
}),
}
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { readFileSync } from "node:fs"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { createTable } from "@conusai-kit/data-kysely"
import { createMysqlTable } from "@conusai-kit/data-kysely/mysql"
import { createMongoCollection } from "@conusai-kit/data-mongo"
import { appUserIdentities, tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
describe("app_user_identity profile contracts", () => {
it.each(["postgres", "mariadb", "mongodb", "convex"] as const)("is registered for %s", (profile) => {
const entities = entitiesOf(buildRegistry([tenancyModule]), profile)
expect(entities.filter((entity) => entity.table === appUserIdentities.table)).toHaveLength(1)
})
it("creates and eagerly backfills the Postgres alias table in a forward migration", () => {
const sql = read("migrations/postgres/0006_app-user-identity.sql")
for (const statement of createTable(appUserIdentities)) expect(sql).toContain(`${statement};`)
expect(sql).toContain('insert into "app_user_identity"')
expect(sql).toContain('from "users"')
expect(sql).toContain('"user_id",\n "migration_id"\n)\nselect\n "id",')
expect(read("migrations/postgres/0001_kernel.sql")).not.toContain('create table "app_user_identity"')
})
it("uses bounded MariaDB unique-key columns and preserves user IDs during backfill", () => {
const sql = read("profiles/mariadb/migrations/0006_app-user-identity.sql")
expect(sql).toContain("`issuer` varchar(255) not null")
expect(sql).toContain("`subject` varchar(255) not null")
expect(sql).toContain("unique (`issuer`, `subject`)")
expect(sql).toContain("insert into `app_user_identity`")
expect(sql).toContain("from `users`")
// The generated table contract has the same fields, constraints and indexes; the migration
// deliberately narrows the two string columns so InnoDB can index their full values.
const generated = createMysqlTable(appUserIdentities).join(";\n")
for (const fragment of [
"create table `app_user_identity`",
"`user_id` char(36) not null",
"`app_user_identity_user_id_idx`",
]) {
expect(generated).toContain(fragment)
expect(sql).toContain(fragment)
}
})
it("commits the generated MongoDB collection and unique/user indexes", () => {
const commands = JSON.parse(read("profiles/mongodb/migrations/0006_app-user-identity.json"))
expect(commands).toEqual(JSON.parse(JSON.stringify(createMongoCollection(appUserIdentities))))
expect(commands[1]).toMatchObject({ createIndex: { unique: true, keys: { issuer: 1, subject: 1 } } })
})
it("generates the Convex table and routes identity operations through private transactions", () => {
const schema = read("convex/schema.ts")
expect(schema).toContain('"app_user_identity": defineTable')
expect(schema).toContain('.index("by_unique_issuer_subject", ["issuer", "subject"])')
expect(schema).toContain('.index("by_userId", ["userId"])')
const system = read("convex/system.ts")
expect(system).toContain("export const appUserResolve = internalMutation")
expect(system).toContain("resolveAppUser(data(ctx)")
expect(system).toContain("export const identityMappingsApply = internalMutation")
expect(system).toContain("applyIdentityMappings(data(ctx)")
expect(system).toContain("export const identityIssuerCoverage = internalQuery")
expect(system).not.toContain("export const userInsert")
expect(system).not.toContain("export const userByIdentity")
})
})
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A role has one name. The invitation page says the role a link joins with by the name Settings > Members gives the
// same role (kit-tenancy's roleName over the built-in roles: a member is an editor), in English and in Lithuanian, from
// the app's own catalogues.
import { createI18n } from "@conusai-kit/i18n"
import { roleName } from "@conusai-kit/tenancy/ui"
import { describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({ pageContext: vi.fn(), call: vi.fn() }))
vi.mock("@tanstack/react-start/server", () => ({
getRequest: () => new Request("http://app.test/"),
setResponseStatus: vi.fn(),
setResponseHeader: vi.fn(),
}))
vi.mock("../apps/web/src/server/page", () => ({ pageContext: mocks.pageContext }))
vi.mock("@conusai-kit/auth", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
loadAuthConfig: () => ({ provider: "oidc", devLogin: true }),
}))
vi.mock("../apps/web/src/server/better-auth", () => ({
MIN_PASSWORD_LENGTH: 12,
betterAuthFormLabels: () => ({ submit: "Go" }),
}))
vi.mock("@app/modules/account/ui", () => ({
analyticsLabels: () => ({}),
deletionLabels: () => ({}),
exportLabels: () => ({}),
pageLabels: () => ({ title: "Account" }),
}))
vi.mock("@conusai-kit/tenancy", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
invitationRead: "invitation.read",
}))
const { appCatalogues } = await import("../apps/web/src/ui/messages")
const { invitationData } = await import("../apps/web/src/server/account-pages")
describe("the invitation page", () => {
it.each(["en", "lt"] as const)("names the role as the members table does, in %s", async (locale) => {
const i18n = createI18n(locale, appCatalogues)
mocks.pageContext.mockResolvedValue({ i18n, caller: { call: mocks.call }, path: "/here", signedIn: false })
for (const [role, roleKey] of [
["viewer", "viewer"],
["member", "editor"],
["admin", "admin"],
["owner", "owner"],
] as const) {
mocks.call.mockResolvedValue({
invitationId: "inv-1",
state: "open",
email: "guest@example.test",
organization: { slug: "acme", name: "Acme" },
role,
roleKey,
expiresAt: "2026-11-10T09:00:00.000Z",
})
const inTable = roleName(i18n, { key: roleKey, name: roleKey, builtIn: true })
expect(inTable, `${locale} ${roleKey} has a translated name`).not.toBe(roleKey)
const { lead } = await invitationData({ org: "acme", token: "token" })
expect(lead).toBe(i18n.message("invite.lead", { email: "guest@example.test", org: "Acme", role: inTable }))
}
})
})
+517
View File
@@ -0,0 +1,517 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Wall three (spec §3.3): the generated suite calls every tenant-scoped procedure of this app's
// registry as a member of organization A with organization B's slugs and ids, and fails any
// procedure that answers with B's data or writes anything at all.
//
// It runs on the in-memory adapter, which is the repository predicate alone: that is wall one.
// Wall two, what Postgres refuses on its own, is proven against a real Postgres by
// @conusai-kit/data-kysely's own gate, and wall three is proven here for every procedure this
// app registers -- including the ones a module adds tomorrow, because the list comes from the
// registry and a procedure without a fixture fails the suite by name.
import { join } from "node:path"
import {
type Actor,
buildRegistry,
type CallerOptions,
Container,
createCaller,
type TransportCaller,
} from "@conusai-kit/core"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { GUIDES_PLATFORM, GUIDES_REGISTRY } from "@conusai-kit/guides" // kit:guides
import { guidesIsolationExempt, guidesIsolationFixtures, recordingGuidesPlatform } from "@conusai-kit/guides/testing" // kit:guides
import { PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
import { SUPPORT_PLATFORM } from "@conusai-kit/support" // kit:support
import * as supportTesting from "@conusai-kit/support/testing" // kit:support
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import { TENANCY_ALSO_CHECK, tenancyIsolationExempt, tenancyIsolationFixtures } from "@conusai-kit/tenancy/testing"
import {
describeIsolation,
fakeJobQueue,
fakeObjectStore,
type IsolationFixture,
type IsolationTenant,
runIsolation,
snapshotOf,
uploadViaProcedures,
} from "@conusai-kit/testing"
import { describe, expect, it, vi } from "vitest"
import { accountIsolationExempt } from "../modules/account/__tests__/exempt.js"
import { documents } from "../modules/documents/index.js" // kit:documents
import { appModules } from "../modules/index.js"
import * as knowledge from "../modules/knowledge/index.js" // kit:knowledge
import { isolationFilesOf } from "./support/module-isolations.js"
const registry = buildRegistry([...appModules])
/** What a module's isolation file exports: the shape this suite reads. */
interface ModuleIsolation {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
}
/**
* The fixtures and the seed rows a module brings in `modules/<id>/__tests__/isolation.ts`, found by
* their place: a module added with `pnpm new:module` is covered without a line here, and a
* procedure it registers with no fixture fails the suite by name. Only registered modules count: a
* module folder kept but not registered serves no procedure (`isolationFilesOf`). (The older
* modules' fixtures are listed below; they move into their own folders as each is next touched.)
*/
const modulesDir = join(import.meta.dirname, "..", "modules")
const moduleIsolations: ModuleIsolation[] = await Promise.all(
isolationFilesOf(
modulesDir,
registry.modules.map((module) => module.id),
).map(async (file) => {
const loaded = (await import(/* @vite-ignore */ file)) as { isolation: ModuleIsolation }
return loaded.isolation
}),
)
const entities = entitiesOf(registry, "postgres")
/** The guides module's platform, recording: a refused cross-tenant call must never reach it. */ // kit:guides
const guidesPlatform = recordingGuidesPlatform() // kit:guides
/** The support module's platform, recording: a refused cross-tenant mint must never reach it. */ // kit:support
const supportPlatform = supportTesting.recordingSupportPlatform() // kit:support
/** The app wired the way apps/web wires it, with the in-memory adapter in place of Postgres. */
function makeApp() {
const data = createMemoryDataSource({ entities })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
// The files module's store: a seeded upload needs one, and a refused cross-tenant call never reaches it.
root.value(OBJECT_STORE, fakeObjectStore())
// A module whose writes enqueue work (a job started by a procedure) needs a queue to be driven at all.
root.value(JOB_QUEUE, fakeJobQueue())
root.value(PROPOSALS_REGISTRY, registry)
root.value(GUIDES_PLATFORM, guidesPlatform) // kit:guides
root.value(GUIDES_REGISTRY, registry) // kit:guides
root.value(SUPPORT_PLATFORM, supportPlatform) // kit:support
// kit:knowledge:begin
// The knowledge module's ports, faked: the search answers nothing (the suite asserts a
// cross-tenant call is refused or answered empty), and the enablement checks pass.
root.value(knowledge.KNOWLEDGE_SEARCH, {
readiness: async () => ({ state: "ready" as const, reasons: [] }),
index: async () => undefined,
search: async () => [],
remove: async () => undefined,
})
root.value(knowledge.KNOWLEDGE_READINESS, { readiness: async () => ({ state: "ready" as const, reasons: [] }) })
root.value(knowledge.KNOWLEDGE_ENABLEMENT, {
checkEligibility: async () => ({ ok: true as const }),
indexingQuota: async () => Number.MAX_SAFE_INTEGER,
})
// kit:knowledge:end
const caller = (
actor: Actor,
admission: Pick<CallerOptions, "prepareServices" | "serviceReadiness"> = {},
): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, ...admission }, { actor })
}
return { data, caller }
}
const app = makeApp()
async function signUp(email: string): Promise<Actor> {
const user = await app.data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
async function tenant(slug: string): Promise<IsolationTenant> {
const owner = await signUp(`owner@${slug}.test`)
const call = app.caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as {
orgId: string
slug: string
workspaceSlug: string
}
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "tenancy",
})) as { projectId: string; slug: string }
const invite = (await call.callByName("tenancy.member.invite", {
org: slug,
email: `invited-${slug}@example.test`,
role: "member",
})) as { invitationId: string; token: string }
const key = (await call.callByName("auth.apiKey.create", { org: slug, name: "ci" })) as { key: { id: string } }
// A custom role of this tenant's own: the role procedures' fixtures name its key from the other side.
const role = (await call.callByName("tenancy.role.create", {
org: slug,
name: `Auditor ${slug}`,
level: "org",
permissions: ["tenancy.org.read", "tenancy.member.read"],
})) as { key: string }
// One upload and one proposal in the project, so a fixture names rows that exist in the other
// tenant: the call has to be refused for being another tenant's, not for naming nothing.
const inProject = { org: slug, workspace: workspace.slug, project: project.slug }
const file = await uploadViaProcedures(call, inProject, {
filename: "seed.pdf",
contentType: "application/pdf",
bytes: new TextEncoder().encode("%PDF-1.4"),
})
// kit:documents:begin
const proposal = (await call.callByName(
"proposals.proposal.create",
{
...inProject,
targetKey: "documents.line",
change: {
lineKey: "a".repeat(64),
label: "Reference",
value: "QR-1",
page: 1,
position: 0,
quote: "Reference: QR-1",
confidence: 0.9,
verification: "verified",
edited: false,
},
evidence: [{ fileId: file.fileId, page: 1, quote: "Reference: QR-1" }],
model: "seed",
promptVersion: "seed.v1",
},
{ idempotencyKey: `seed.${slug}` },
)) as { proposalId: string }
// A document row, as the OCR job makes one: the live collection's read-back names it by its id.
const documentRow = await app.data
.forScope({
level: "project",
actor: owner,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
})
.of(documents)
.insert({
fileId: file.fileId,
filename: "seed.pdf",
status: "processing",
ocrJobId: null,
pageCount: null,
proposedCount: null,
failureCode: null,
skippedPages: null,
finishedAt: null,
})
// kit:documents:end
// kit:knowledge:begin
// A knowledge_document row, as an index request makes one (indexing needs the module switched on, which
// the seeding caller cannot do): the live collection's read-back names it by its id.
const knowledgeDocument = await app.data
.forScope({
level: "project",
actor: owner,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
})
.of(knowledge.documents)
.insert({
collectionName: "contracts",
fileId: file.fileId,
filename: "seed.pdf",
status: "queued",
error: null,
indexedAt: null,
requestedBy: owner.userId,
})
// kit:knowledge:end
// What each module that brings fixtures seeds in this tenant: its ids are what the other tenant's fixtures name.
const seeded: Record<string, string> = {}
for (const isolation of moduleIsolations) Object.assign(seeded, await isolation.seed(call, inProject))
return {
orgSlug: org.slug,
workspaceSlug: workspace.slug,
projectSlug: project.slug,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
userId: owner.userId,
email: `owner@${slug}.test`,
extra: {
invitationId: invite.invitationId,
token: invite.token,
roleKey: role.key,
apiKeyId: key.key.id,
fileId: file.fileId,
proposalId: proposal.proposalId, // kit:documents
documentId: documentRow.id, // kit:documents
knowledgeDocumentId: knowledgeDocument.id, // kit:knowledge
...seeded,
},
}
}
let seeded: Promise<{ a: IsolationTenant; b: IsolationTenant }> | undefined
const tenants = () => {
seeded ??= (async () => ({ a: await tenant("alpha"), b: await tenant("bravo") }))()
return seeded
}
/** The three slugs a project-scoped procedure's input carries, from the tenant it must not reach. */
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
/** The other tenant's seeded rows; the ghost tenant has none, so its ids are well-formed and absent. */
const NOWHERE = "00000000-0000-0000-0000-000000000000"
const fileOf = (t: IsolationTenant) => t.extra.fileId ?? NOWHERE
const proposalOf = (t: IsolationTenant) => t.extra.proposalId ?? NOWHERE
const documentOf = (t: IsolationTenant) => t.extra.documentId ?? NOWHERE // kit:documents
/** One fixture per tenant-scoped procedure, built from the *other* tenant's ids. */
const fixtures: Record<string, IsolationFixture> = {
// The tenancy module's, which this suite cannot be run without (one list, in @conusai-kit/tenancy/testing).
...tenancyIsolationFixtures(),
...Object.assign({}, ...moduleIsolations.map((isolation) => isolation.fixtures)),
...guidesIsolationFixtures, // kit:guides
...supportTesting.supportIsolationFixtures, // kit:support
"auth.apiKey.create": { input: (t) => ({ org: t.orgSlug, name: "intruder" }) },
"auth.apiKey.list": { input: (t) => ({ org: t.orgSlug }) },
"auth.apiKey.revoke": {
input: (t) => ({ org: t.orgSlug, id: t.extra.apiKeyId ?? "00000000-0000-0000-0000-000000000000" }),
},
"files.file.beginUpload": {
input: (t) => ({ ...inProjectOf(t), filename: "intruder.pdf", contentType: "application/pdf", size: 8 }),
},
"files.file.putPart": {
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), part: 1, body: new TextEncoder().encode("%PDF-1.4") }),
},
"files.file.completeUpload": {
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), parts: [{ number: 1, etag: "etag-intruder" }] }),
},
"files.file.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"files.file.download": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"files.file.list": { input: (t) => inProjectOf(t) },
"files.file.delete": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
"proposals.proposal.create": {
input: (t) => ({
...inProjectOf(t),
targetKey: "documents.line",
change: {
lineKey: "b".repeat(64),
label: "Intruder",
value: "x",
page: 1,
position: 0,
quote: "Intruder: x",
confidence: 0.5,
edited: false,
},
// Nothing of the other tenant's: `create` reads no id it is given, so the only cross-tenant
// case it has is the slugs, which the resolver refuses. An id of B's here would be echoed
// back in the answer, which is the caller's own input and not B's data.
evidence: [{ fileId: NOWHERE, page: 1 }],
model: "intruder",
promptVersion: "intruder.v1",
}),
},
"proposals.proposal.edit": {
input: (t) => ({
...inProjectOf(t),
proposalId: proposalOf(t),
expectedVersion: 1,
change: {
lineKey: "c".repeat(64),
label: "Intruder",
value: "x",
page: 1,
position: 0,
quote: "Intruder: x",
confidence: 0.5,
edited: true,
},
}),
},
"proposals.proposal.get": { input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t) }) },
"proposals.proposal.list": { input: (t) => inProjectOf(t) },
"proposals.proposal.approve": {
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), expectedVersion: 1 }),
},
"proposals.proposal.approveMany": {
input: (t) => {
const proposalId = proposalOf(t)
return { ...inProjectOf(t), proposalIds: [proposalId], expectations: { [proposalId]: { expectedVersion: 1 } } }
},
},
"proposals.proposal.reject": {
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), reason: "intruder" }),
},
// kit:documents:begin
"documents.document.list": { input: (t) => inProjectOf(t) },
"documents.document.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
// The live collection's read-back: another tenant's document id answers nothing.
"documents.document.getMany": { input: (t) => ({ ...inProjectOf(t), ids: [documentOf(t)] }) },
"documents.line.edit": {
input: (t) => ({ ...inProjectOf(t), lineId: proposalOf(t), expectedVersion: 1, label: "Intruder", value: "x" }),
},
"documents.export.create": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
// kit:documents:end
"branding.brand.get": { input: (t) => ({ org: t.orgSlug }) },
"branding.brand.set": { input: (t) => ({ org: t.orgSlug, primary: "#000000", displayName: "Intruder" }) },
"branding.logo.set": {
input: (t) => ({ org: t.orgSlug, variant: "light", contentType: "image/png", body: "iVBORw0KGgoAAAAA" }),
},
"branding.logo.clear": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
"branding.logo.get": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
}
const alsoCheck = [...TENANCY_ALSO_CHECK]
const exempt = {
...tenancyIsolationExempt,
"auth.session.current": "it answers only about the signed-in user's own row",
...accountIsolationExempt,
...guidesIsolationExempt, // kit:guides
...supportTesting.supportIsolationExempt, // kit:support
}
const options = async () => {
const { a, b } = await tenants()
return {
registry,
seed: async () => ({ a, b }),
caller: (actor: Actor) => app.caller(actor),
snapshot: () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
fixtures,
alsoCheck,
exempt,
}
}
describeIsolation(
{
registry,
seed: async () => tenants(),
caller: (actor) => ({
call: async () => undefined as never,
callByName: async (name, input) => app.caller(actor).callByName(name, input),
}),
snapshot: async () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
fixtures,
alsoCheck,
exempt,
},
{ describe, it },
)
describe("the generated isolation suite", () => {
it("checks current membership before preparing or observing service admission", async () => {
const { a, b } = await tenants()
const prepareServices = vi.fn()
const observe = vi.fn(async () => {
throw new Error("synthetic service unavailable")
})
const admission = { prepareServices, serviceReadiness: { profile: "postgres" as const, observe } }
const outsider = app.caller({ userId: a.userId }, admission)
await expect(outsider.callByName("inventory.item.list", inProjectOf(b))).rejects.toMatchObject({ status: 404 })
expect(prepareServices).not.toHaveBeenCalled()
expect(observe).not.toHaveBeenCalled()
const member = await signUp("removed-member@isolation.test")
const owner = app.caller({ userId: a.userId })
const invite = (await owner.callByName("tenancy.member.invite", {
org: a.orgSlug,
email: "removed-member@isolation.test",
role: "viewer",
})) as { token: string }
await app.caller(member).callByName("tenancy.invitation.accept", { org: a.orgSlug, token: invite.token })
await expect(app.caller(member, admission).callByName("inventory.item.list", inProjectOf(a))).rejects.toThrow()
expect(prepareServices).toHaveBeenCalledOnce()
expect(observe).toHaveBeenCalledOnce()
await owner.callByName("tenancy.member.remove", { org: a.orgSlug, userId: member.userId })
prepareServices.mockClear()
observe.mockClear()
// A new request must resolve fresh membership, before consulting the unavailable service.
await expect(app.caller(member, admission).callByName("inventory.item.list", inProjectOf(a))).rejects.toMatchObject(
{ status: 404 },
)
expect(prepareServices).not.toHaveBeenCalled()
expect(observe).not.toHaveBeenCalled()
})
it("covers every procedure this app registers", async () => {
const result = await runIsolation(await options())
expect(result.failures).toEqual([])
// One case per live collection too (TS6.5): its read-back, by the collection's name.
expect(result.passed.length).toBe(
registry.isolationFixtures.length + alsoCheck.length + Object.keys(registry.collections).length,
)
expect(result.passed).toContain("live notes.note") // kit:notes
})
// kit:notes:begin
it("covers every procedure of the notes and files modules, by name", async () => {
const result = await runIsolation(await options())
const covered = new Set(result.passed)
const names = [...registry.procedures.keys()].filter((name) => /^(notes|files)\./.test(name))
expect(names.length).toBe(15)
for (const name of names) expect(covered.has(name), name).toBe(true)
})
it("fails, naming it, when a module's own fixture is missing", async () => {
const base = await options()
const { "notes.attachment.remove": _removed, ...rest } = fixtures
const result = await runIsolation({ ...base, fixtures: rest })
expect(result.failures.map((failure) => failure.procedure)).toEqual(["notes.attachment.remove"])
})
// kit:notes:end
// kit:guides:begin
it("reaches the guides platform with no cross-tenant call", async () => {
await runIsolation(await options())
expect(guidesPlatform.calls).toEqual([])
})
// kit:guides:end
// kit:support:begin
it("reaches the support platform with no cross-tenant call", async () => {
await runIsolation(await options())
expect(supportPlatform.calls).toEqual([])
})
// kit:support:end
it("fails, naming it, when a procedure has no fixture", async () => {
const base = await options()
const { "tenancy.member.list": _removed, ...rest } = fixtures
const result = await runIsolation({ ...base, fixtures: rest })
expect(result.failures.map((failure) => failure.procedure)).toEqual(["tenancy.member.list"])
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(
(await importOriginal<typeof import("../modules/inclusion.js")>()).OPTIONAL_MODULE_IDS,
),
}))
+323
View File
@@ -0,0 +1,323 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The committed kernel schema, and the rule that keeps it committable.
//
// `migrations/postgres/0001_kernel.sql` is `schemaStatements()` over the kernel's own entities,
// written once. It is a snapshot of the *kernel's* tables alone, not of the whole registry, so
// adding a module does not change this file -- a module ships its own numbered migration, which
// is what a forward-only runner needs (it refuses a migration whose text changed after it ran).
// A kit upgrade that changes a kernel table does change it, and that is exactly the moment a
// customer has to see a new migration in review.
import { readFileSync } from "node:fs"
import { authModule } from "@conusai-kit/auth"
import { buildRegistry } from "@conusai-kit/core"
import { type AnyEntityDef, defineEntity, entitiesOf, managedColumnsFor } from "@conusai-kit/data"
import {
columnName,
createTable,
enableRowLevelSecurity,
isNullable,
postgresType,
quoted,
schemaStatements,
} from "@conusai-kit/data-kysely"
import { createMysqlTable, mysqlQuoted, mysqlType } from "@conusai-kit/data-kysely/mysql"
import { createMongoCollection, type MongoCommand, mongoValidator } from "@conusai-kit/data-mongo"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import {
appUserIdentities,
invitations,
orgMemberships,
projectMemberships,
projects,
tenancyModule,
tenancyRoles,
workspaceMemberships,
} from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { z } from "zod"
import { kernelSchema } from "../apps/worker/src/migrations.js"
import { SPDX_LINE } from "../scripts/licence.mjs"
/**
* The columns the R-wave roles migration (0008) added to tables 0001 created: a role key on the
* memberships and invitations, a project's visibility. 0001 is the snapshot from before them.
*/
const ADDED_BY_0008: ReadonlyArray<readonly [AnyEntityDef, string]> = [
[orgMemberships, "roleKey"],
[workspaceMemberships, "roleKey"],
[invitations, "roleKey"],
[projects, "visibility"],
]
/** The column 0009 added to a table 0001 created: a project's description. */
const ADDED_BY_0009: ReadonlyArray<readonly [AnyEntityDef, string]> = [[projects, "description"]]
/** The column 0010 added to a table 0001 created: when an invitation was revoked or re-issued. */
const ADDED_BY_0010: ReadonlyArray<readonly [AnyEntityDef, string]> = [[invitations, "revokedAt"]]
const fieldsOf = (added: ReadonlyArray<readonly [AnyEntityDef, string]>, entity: AnyEntityDef) =>
added.filter(([changed]) => changed.table === entity.table).map(([, field]) => field)
/** An entity as 0001 created it: without the fields a later migration added, nor their indexes. */
const asOf0001 = (entity: AnyEntityDef): AnyEntityDef =>
without(entity, [
...fieldsOf(ADDED_BY_0008, entity),
...fieldsOf(ADDED_BY_0009, entity),
...fieldsOf(ADDED_BY_0010, entity),
])
/** An entity as 0008 left it: a committed validator is the whole of its table at that point. */
const asOf0008 = (entity: AnyEntityDef): AnyEntityDef =>
without(entity, [...fieldsOf(ADDED_BY_0009, entity), ...fieldsOf(ADDED_BY_0010, entity)])
/** An entity as 0009 left it. */
const asOf0009 = (entity: AnyEntityDef): AnyEntityDef => without(entity, fieldsOf(ADDED_BY_0010, entity))
function without(entity: AnyEntityDef, later: readonly string[]): AnyEntityDef {
if (later.length === 0) return entity
const schema = entity.schema
if (!(schema instanceof z.ZodObject)) throw new Error(`${entity.table}: an entity's schema is an object`)
const managed = managedColumnsFor(entity.tenancy)
const fields = Object.fromEntries(
Object.entries(schema.shape).filter(([field]) => !managed.includes(field) && !later.includes(field)),
)
const before = (groups: readonly (readonly string[])[]) =>
groups.filter((group) => !group.some((field) => later.includes(field))).map((group) => [...group])
return defineEntity({
name: entity.name,
table: entity.table,
tenancy: entity.tenancy,
fields,
unique: before(entity.unique),
indexes: before(entity.indexes),
})
}
// Tenancy and sign-in alone: later kit tables have their own forward migrations, because 0001 is
// never edited once it has run. app_user_identity belongs to 0006 and the custom roles and project
// memberships to 0008, not this historical snapshot.
const LATER_TABLES = new Set([appUserIdentities.table, tenancyRoles.table, projectMemberships.table])
const kernelEntities = [...entitiesOf(buildRegistry([tenancyModule, authModule]), "postgres"), ...KIT_JOBS_ENTITIES]
.filter((entity) => !LATER_TABLES.has(entity.table))
.map(asOf0001)
describe("the kernel migration", () => {
it("matches the committed 0001_kernel.sql", async () => {
// The file leads with the template's SPDX line (AT5.1); the runner reads it as the comment it is.
await expect(`-- ${SPDX_LINE}\n${kernelSchema(kernelEntities)}`).toMatchFileSnapshot(
"../migrations/postgres/0001_kernel.sql",
)
})
it("puts forced row-level security on every tenant table", () => {
const sql = schemaStatements(kernelEntities).join(";\n")
for (const entity of kernelEntities) {
if (entity.tenancy === "global") continue
expect(sql, `${entity.table} is not forced`).toContain(`alter table "${entity.table}" force row level security`)
expect(sql).toContain(`create policy "${entity.table}_tenant" on "${entity.table}"`)
}
})
it("compares a tenant row against the transaction's organization and nothing else", () => {
const sql = schemaStatements(kernelEntities).join(";\n")
// The policy reads NULLIF(current_setting(...), '')::uuid: a pooled connection whose previous
// transaction set app.org_id sees no rows rather than an error (kit-data-kysely's decision 2).
expect(sql).toContain("nullif(current_setting('app.org_id', true), '')::uuid")
})
})
describe("the custom roles migration (R-wave, ADR-062)", () => {
const shapeOf = (entity: AnyEntityDef) => {
if (!(entity.schema instanceof z.ZodObject)) throw new Error(`${entity.table}: an entity's schema is an object`)
return entity.schema.shape
}
it("is 0008_tenancy-roles.sql: the two new tables, the added columns with their indexes, and forced RLS", async () => {
const statements = [
...createTable(tenancyRoles),
...createTable(projectMemberships),
...ADDED_BY_0008.map(([entity, field]) => {
const type = shapeOf(entity)[field]
// Added to tables that already hold rows: a forward migration cannot add a NOT NULL column.
expect(isNullable(type), `${entity.table}.${field}`).toBe(true)
return `alter table ${quoted(entity.table)} add column ${quoted(columnName(field))} ${postgresType(type)}`
}),
...ADDED_BY_0008.flatMap(([entity, field]) =>
createTable(entity).filter(
(statement) => statement.startsWith("create index") && statement.includes(`, ${quoted(columnName(field))})`),
),
),
...enableRowLevelSecurity(tenancyRoles),
...enableRowLevelSecurity(projectMemberships),
]
await expect(
`-- ${SPDX_LINE}\n${statements.map((statement) => `${statement};`).join("\n\n")}\n`,
).toMatchFileSnapshot("../migrations/postgres/0008_tenancy-roles.sql")
})
it("forces row-level security on the organization column of both new tables", () => {
const sql = readFileSync(new URL("../migrations/postgres/0008_tenancy-roles.sql", import.meta.url), "utf8")
for (const table of [tenancyRoles.table, projectMemberships.table]) {
expect(sql).toContain(`alter table "${table}" force row level security`)
expect(sql).toContain(`create policy "${table}_tenant" on "${table}"`)
}
expect(sql).not.toContain("drop ")
})
})
// The same migration on the dialect profiles (ADR-062: every profile has the new tables under its own
// isolation mechanism). Neither engine has row-level security: the wall is the repository's tenant
// predicate on `org_id`, which the shared contract suites prove, and each unique group is scoped by the
// organization column as every kernel table's is.
describe("the custom roles migration on MariaDB and MongoDB", () => {
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
const addedColumns = (entity: AnyEntityDef) =>
ADDED_BY_0008.filter(([added]) => added === entity).map(([, field]) => columnName(field))
it("is profiles/mariadb/migrations/0008_tenancy-roles.sql: the two tables, the added columns and their indexes", async () => {
const statements = [
...createMysqlTable(tenancyRoles),
...createMysqlTable(projectMemberships),
...ADDED_BY_0008.map(([entity, field]) => {
const type = shapeOfEntity(entity)[field]
expect(isNullable(type), `${entity.table}.${field}`).toBe(true)
return `alter table ${mysqlQuoted(entity.table)} add column ${mysqlQuoted(columnName(field))} ${mysqlType(type)}`
}),
...ADDED_BY_0008.flatMap(([entity, field]) =>
createMysqlTable(entity).filter(
(statement) =>
statement.startsWith("create index") &&
new RegExp(`, ${mysqlQuoted(columnName(field))}(\\(191\\))?\\)$`).test(statement),
),
),
]
await expect(
`-- ${SPDX_LINE}\n${statements.map((statement) => `${statement};`).join("\n\n")}\n`,
).toMatchFileSnapshot("../profiles/mariadb/migrations/0008_tenancy-roles.sql")
})
it("scopes both new tables' unique groups by the organization, and drops nothing", () => {
const sql = read("profiles/mariadb/migrations/0008_tenancy-roles.sql")
expect(sql).toContain("unique (`org_id`, `key`)")
expect(sql).toContain("unique (`org_id`, `project_id`, `user_id`)")
expect(sql).not.toContain("drop ")
})
it("is profiles/mongodb/migrations/0008_tenancy-roles.json: two collections, the four validators with the new fields, their indexes", () => {
const changed = [...new Set(ADDED_BY_0008.map(([entity]) => entity))]
const expected: MongoCommand[] = [
...createMongoCollection(tenancyRoles),
...createMongoCollection(projectMemberships),
// A collection's validator refuses a field it does not list (additionalProperties: false), so the
// added fields arrive as each collection's whole current validator.
...changed.map((entity) => ({ collMod: entity.table, validator: mongoValidator(asOf0008(entity)) })),
...changed.flatMap((entity) =>
createMongoCollection(entity).filter(
(command) =>
"createIndex" in command &&
addedColumns(entity).some((column) => Object.keys(command.createIndex.keys).includes(column)),
),
),
]
// Compared as data: the committed file is formatted by Biome, the generator's output is not. It ran before
// a nullable enum listed null, which 0009 adds, and a file that ran is never edited.
expect(JSON.parse(read("profiles/mongodb/migrations/0008_tenancy-roles.json"))).toEqual(
JSON.parse(
JSON.stringify(expected, (_, item) => (Array.isArray(item) ? item.filter((entry) => entry !== null) : item)),
),
)
for (const entity of changed) {
const validator = JSON.stringify(mongoValidator(asOf0008(entity)))
for (const column of addedColumns(entity)) expect(validator, `${entity.table}.${column}`).toContain(`"${column}"`)
}
})
it("is profiles/mongodb/migrations/0009_nullable-enums.json: an invitation with no workspace may have no workspace role", () => {
expect(JSON.parse(read("profiles/mongodb/migrations/0009_nullable-enums.json"))).toEqual([
{ collMod: invitations.table, validator: mongoValidator(asOf0009(invitations)) },
])
const { properties } = mongoValidator(asOf0009(invitations)).$jsonSchema as { properties: Record<string, unknown> }
expect(properties.workspace_role).toEqual({
bsonType: ["string", "null"],
enum: ["viewer", "editor", "admin", null],
})
})
})
// One added column on every profile. Convex takes it from the generated schema (`convex/schema.ts`,
// which the Convex generator writes from the entities), and the Postgres reader's whole-row grant is
// built from the entity after these files ran.
describe("the project description migration", () => {
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
const shape = shapeOfEntity(projects)
it("adds a nullable column to the existing table: a forward migration over rows that exist", () => {
for (const [entity, field] of ADDED_BY_0009) {
expect(isNullable(shapeOfEntity(entity)[field]), `${entity.table}.${field}`).toBe(true)
}
})
it("is 0009_project-description.sql on Postgres", async () => {
const statement = `alter table ${quoted(projects.table)} add column ${quoted(columnName("description"))} ${postgresType(shape.description)}`
await expect(`-- ${SPDX_LINE}\n${statement};\n`).toMatchFileSnapshot(
"../migrations/postgres/0009_project-description.sql",
)
})
it("is profiles/mariadb/migrations/0009_project-description.sql", async () => {
const statement = `alter table ${mysqlQuoted(projects.table)} add column ${mysqlQuoted(columnName("description"))} ${mysqlType(shape.description)}`
await expect(`-- ${SPDX_LINE}\n${statement};\n`).toMatchFileSnapshot(
"../profiles/mariadb/migrations/0009_project-description.sql",
)
})
it("is profiles/mongodb/migrations/0009_project-description.json: the collection's whole current validator", () => {
// A collection's validator refuses a field it does not list (additionalProperties: false).
const expected: MongoCommand[] = [{ collMod: projects.table, validator: mongoValidator(asOf0009(projects)) }]
expect(JSON.parse(read("profiles/mongodb/migrations/0009_project-description.json"))).toEqual(
JSON.parse(JSON.stringify(expected)),
)
expect(JSON.stringify(mongoValidator(projects))).toContain('"description"')
})
})
function shapeOfEntity(entity: AnyEntityDef) {
if (!(entity.schema instanceof z.ZodObject)) throw new Error(`${entity.table}: an entity's schema is an object`)
return entity.schema.shape
}
// An invitation that was revoked keeps its row, so its link can say so. One nullable column on every
// profile; Convex takes it from the generated schema.
describe("the invitation revoked migration", () => {
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
const shape = shapeOfEntity(invitations)
it("adds a nullable column to the existing table: a forward migration over rows that exist", () => {
for (const [entity, field] of ADDED_BY_0010) {
expect(isNullable(shapeOfEntity(entity)[field]), `${entity.table}.${field}`).toBe(true)
}
})
it("is 0010_invitation-revoked.sql on Postgres", async () => {
const statement = `alter table ${quoted(invitations.table)} add column ${quoted(columnName("revokedAt"))} ${postgresType(shape.revokedAt)}`
await expect(`-- ${SPDX_LINE}\n${statement};\n`).toMatchFileSnapshot(
"../migrations/postgres/0010_invitation-revoked.sql",
)
})
it("is profiles/mariadb/migrations/0010_invitation-revoked.sql", async () => {
const statement = `alter table ${mysqlQuoted(invitations.table)} add column ${mysqlQuoted(columnName("revokedAt"))} ${mysqlType(shape.revokedAt)}`
await expect(`-- ${SPDX_LINE}\n${statement};\n`).toMatchFileSnapshot(
"../profiles/mariadb/migrations/0010_invitation-revoked.sql",
)
})
it("is profiles/mongodb/migrations/0010_invitation-revoked.json: the collection's whole current validator", () => {
const expected: MongoCommand[] = [{ collMod: invitations.table, validator: mongoValidator(invitations) }]
expect(JSON.parse(read("profiles/mongodb/migrations/0010_invitation-revoked.json"))).toEqual(
JSON.parse(JSON.stringify(expected)),
)
expect(JSON.stringify(mongoValidator(invitations))).toContain('"revoked_at"')
})
})
+37
View File
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Retrieval is one platform service (ADR-061, RAG R9): the knowledge module's backend is the platform's
// knowledge service, an app keeps no vector store of its own, and a deployment that still names the old
// backend is told so by name instead of quietly getting another one.
import { readFileSync } from "node:fs"
import { KNOWLEDGE_BACKEND_VAR, KNOWLEDGE_BACKENDS, knowledgeBackendOf } from "@app/config"
import { describe, expect, it } from "vitest"
const read = (path: string): string => readFileSync(new URL(`../${path}`, import.meta.url), "utf8")
describe("the knowledge backend", () => {
it("is the platform's by default", () => {
expect(knowledgeBackendOf({})).toBe("platform")
expect(knowledgeBackendOf({ KNOWLEDGE_BACKEND: "" })).toBe("platform")
expect(knowledgeBackendOf({ KNOWLEDGE_BACKEND: " Platform " })).toBe("platform")
})
it("has no local backend to choose, and refuses the retired one by the variable's name", () => {
expect(KNOWLEDGE_BACKENDS).toEqual(["platform"])
expect(() => knowledgeBackendOf({ KNOWLEDGE_BACKEND: "vectors" })).toThrow(KNOWLEDGE_BACKEND_VAR)
})
it("leaves no vector store in the apps, their dependencies or the deployment contract", () => {
for (const file of [
"apps/web/src/server/ports.ts",
"apps/worker/src/ports.ts",
"apps/web/package.json",
"apps/worker/package.json",
"pnpm-workspace.yaml",
"compose.dev.yaml",
".env.example",
]) {
expect(read(file), file).not.toMatch(/qdrant|VECTOR_STORE|pgvector/i)
}
})
})
+195
View File
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The licence layout (plan item AT5.1, spec section 14.3): LICENSE-TEMPLATE says what it is, the
// customer's LICENSE is a stub, NOTICE and TEMPLATE-FILES exist and agree with the tree, and every
// template file that can hold a comment carries the SPDX header. The negative controls make the check
// fail where it should: a scaffold file that loses its header, a file nobody listed, a listed file that
// is gone.
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import {
hasHeader,
headerFor,
LIST_FILE,
listText,
parseList,
problems,
SPDX_ID,
SPDX_LINE,
templateFiles,
withHeader,
write,
} from "../scripts/licence.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const read = (path: string) => readFileSync(join(APP_ROOT, path), "utf8")
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
/** A small tree shaped like the template: a scaffold, a module and the layout's own files. */
function fixture(files: Record<string, string>): string {
const root = mkdtempSync(join(tmpdir(), "licence-"))
made.push(root)
const all: Record<string, string> = {
LICENSE: "yours\n",
"LICENSE-TEMPLATE": "placeholder\n",
NOTICE: "notice\n",
"THIRD-PARTY-NOTICES": "none\n",
"package.json": "{}\n",
"src/a.ts": "export const a = 1\n",
"modules/notes/index.ts": "export const notes = 1\n",
...files,
}
for (const [path, text] of Object.entries(all)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), text)
}
write(root)
return root
}
describe("what the tool leaves alone", () => {
it("claims neither a repository's history nor the app's own documents", () => {
const root = fixture({
".git/hooks/pre-commit.sh": "exit 0\n",
"docs/domain-start.md": "the template's\n",
"docs/tender/private.md": "the customer's\n",
})
const files = templateFiles(root)
expect(files).toContain("docs/domain-start.md")
expect(files.filter((file) => file.startsWith(".git/") || file.startsWith("docs/tender"))).toEqual([])
expect(readFileSync(join(root, "docs/tender/private.md"), "utf8")).toBe("the customer's\n")
})
it("claims a git worktree's history neither: there `.git` is a file (template lesson core-8)", () => {
const root = fixture({ ".git": "gitdir: /elsewhere/.git/worktrees/app\n" })
expect(templateFiles(root)).not.toContain(".git")
})
})
describe("this template's licence layout", () => {
it("is whole: every file is listed, every file that can hold a header has it, and the four documents exist", () => {
expect(problems(APP_ROOT)).toEqual([])
})
it("says in LICENSE-TEMPLATE that it is a placeholder, until counsel decides (AT-O2)", () => {
const text = read("LICENSE-TEMPLATE")
expect(text).toMatch(/PLACEHOLDER/)
expect(text).toContain("AT-O2")
expect(text).toContain(SPDX_ID)
expect(text).toMatch(/counsel/i)
})
it("keeps LICENSE the customer's own, a stub that names what it does not cover", () => {
const text = read("LICENSE")
expect(text).toMatch(/This file is yours/)
expect(text).toContain("LICENSE-TEMPLATE")
expect(text).toContain("TEMPLATE-FILES")
})
it("lists the template's files and not the customer's", () => {
const listed = parseList(read(LIST_FILE))
expect(listed).toEqual(templateFiles(APP_ROOT))
expect(listed).toContain("apps/web/src/routes/__root.tsx")
expect(listed).toContain("LICENSE-TEMPLATE")
expect(listed).toContain(LIST_FILE)
expect(listed).not.toContain("LICENSE")
expect(listed).not.toContain("pnpm-lock.yaml")
expect(listed.some((path) => path.startsWith("modules/"))).toBe(false)
expect(listed).not.toContain(".conusai/template.json")
})
it("puts the header first in a TypeScript file, a kernel migration, a workflow, the Dockerfile and the guide", () => {
expect(read("Dockerfile").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
expect(read("test/registry.test.ts").split("\n")[0]).toBe(`// ${SPDX_LINE}`)
expect(read("migrations/postgres/0001_kernel.sql").split("\n")[0]).toBe(`-- ${SPDX_LINE}`)
expect(read(".gitea/workflows/ci.yml").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
expect(read("AGENTS.md").split("\n")[0]).toBe(`<!-- ${SPDX_LINE} -->`)
})
})
describe("the header", () => {
it("is a comment of the file's own kind, and none for a file that cannot hold one", () => {
expect(headerFor("a/b.ts")).toBe(`// ${SPDX_LINE}`)
expect(headerFor("a/b.mjs")).toBe(`// ${SPDX_LINE}`)
expect(headerFor("a/b.css")).toBe(`/* ${SPDX_LINE} */`)
expect(headerFor("a/b.sql")).toBe(`-- ${SPDX_LINE}`)
expect(headerFor("a/b.md")).toBe(`<!-- ${SPDX_LINE} -->`)
expect(headerFor("compose.yaml")).toBe(`# ${SPDX_LINE}`)
expect(headerFor("Dockerfile")).toBe(`# ${SPDX_LINE}`)
expect(headerFor(".env.example")).toBe(`# ${SPDX_LINE}`)
expect(headerFor("package.json")).toBeNull()
expect(headerFor("llms.txt")).toBeNull()
expect(headerFor("LICENSE-TEMPLATE")).toBeNull()
})
it("goes after a shebang, and is added once", () => {
const script = "#!/usr/bin/env bash\nset -e\n"
const once = withHeader(script, "run.sh")
expect(once).toBe(`#!/usr/bin/env bash\n# ${SPDX_LINE}\nset -e\n`)
expect(withHeader(once, "run.sh")).toBe(once)
expect(hasHeader(once, "run.sh")).toBe(true)
expect(hasHeader(script, "run.sh")).toBe(false)
})
it("goes after the frontmatter of a Markdown file that opens with one (an agent skill), and is added once", () => {
const skill = "---\nname: fix\ndescription: Fix it.\n---\n# Fix\n"
const once = withHeader(skill, ".claude/skills/fix/SKILL.md")
expect(once).toBe(`---\nname: fix\ndescription: Fix it.\n---\n<!-- ${SPDX_LINE} -->\n# Fix\n`)
expect(withHeader(once, ".claude/skills/fix/SKILL.md")).toBe(once)
expect(hasHeader(once, ".claude/skills/fix/SKILL.md")).toBe(true)
// A header above the frontmatter would hide the skill from its tool, so it does not count.
expect(hasHeader(`<!-- ${SPDX_LINE} -->\n${skill}`, ".claude/skills/fix/SKILL.md")).toBe(true)
expect(hasHeader(skill, ".claude/skills/fix/SKILL.md")).toBe(false)
})
})
describe("the check fails where it should", () => {
it("passes a tree that `write` has laid out, and ignores modules/** and LICENSE", () => {
const root = fixture({})
expect(problems(root)).toEqual([])
expect(parseList(readFileSync(join(root, LIST_FILE), "utf8"))).toEqual([
"LICENSE-TEMPLATE",
"NOTICE",
"TEMPLATE-FILES",
"THIRD-PARTY-NOTICES",
"package.json",
"src/a.ts",
])
expect(readFileSync(join(root, "modules/notes/index.ts"), "utf8")).not.toContain("SPDX")
expect(readFileSync(join(root, "LICENSE"), "utf8")).toBe("yours\n")
})
it("fails, naming the file, when a scaffold file loses its header", () => {
const root = fixture({})
writeFileSync(join(root, "src/a.ts"), "export const a = 1\n")
expect(problems(root)).toEqual([`src/a.ts has no "${SPDX_LINE}" header`])
})
it("fails when a file is not in TEMPLATE-FILES, and when a listed file is gone", () => {
const root = fixture({})
writeFileSync(join(root, "src/new.ts"), `// ${SPDX_LINE}\nexport {}\n`)
expect(problems(root)).toEqual(["TEMPLATE-FILES does not list src/new.ts"])
rmSync(join(root, "src/new.ts"))
rmSync(join(root, "src/a.ts"))
expect(problems(root)).toEqual(["TEMPLATE-FILES lists src/a.ts, which is not a template file"])
})
it("fails when one of the four documents is missing", () => {
const root = fixture({})
rmSync(join(root, "NOTICE"))
expect(problems(root)).toContain("NOTICE is missing")
})
it("is what listText writes: comments, then one path a line", () => {
const text = listText(["a", "b"])
expect(parseList(text)).toEqual(["a", "b"])
expect(text.startsWith("# ")).toBe(true)
})
})
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A write never reloads to show itself (AGENTS.md, "The recipe for a module", steps 5 and 9).
//
// - No module reloads the document: `location.reload()` throws the page's state away and reads every
// loader again, where a live read changes only what changed.
// - A module that declares a live collection writes through `useWrite`, which resolves once the
// organization's live stream has seen the write, so every live read on the page already shows it.
// Its UI re-runs no loader after a write (`usePageNavigation().refresh()`, `router.invalidate()`):
// a page of it reads through `useLive` or a `useRead` kept live by the module's entities.
//
// Which modules declare a collection comes from their own manifests, every module whose source is
// here, selected or not: a module that is switched on later is held to the same rule.
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
import { join, relative } from "node:path"
import type { ModuleManifest } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
const root = new URL("..", import.meta.url).pathname
const modulesDir = join(root, "modules")
const RELOAD = /\blocation\s*(?:\.\s*reload\b|\[\s*["'`]reload["'`]\s*\])/
const LOADER_RERUN = /\.\s*refresh\s*\(|\brouter\s*\.\s*invalidate\s*\(/
function sources(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name)
if (statSync(path).isDirectory()) return name === "node_modules" || name === "dist" ? [] : sources(path)
return /\.(?:ts|tsx|mts)$/.test(name) ? [path] : []
})
}
const moduleIds = readdirSync(modulesDir).filter((name) => existsSync(join(modulesDir, name, "index.ts")))
const isManifest = (value: unknown): value is ModuleManifest =>
typeof value === "object" && value !== null && "id" in value && "procedures" in value
/** The ids of the source modules whose manifests declare at least one live collection. */
async function modulesWithCollections(): Promise<string[]> {
const found = new Set<string>()
for (const id of moduleIds) {
const exports = (await import(join(modulesDir, id, "index.ts"))) as Record<string, unknown>
for (const value of Object.values(exports)) {
if (isManifest(value) && (value.collections?.length ?? 0) > 0) found.add(id)
}
}
return [...found].sort()
}
/** `file:line: text` for each line of `files` that matches `pattern`, outside comments. */
function offending(files: readonly string[], pattern: RegExp): string[] {
return files.flatMap((file) =>
readFileSync(file, "utf8")
.split("\n")
.flatMap((line, index) => {
const code = line.replace(/\/\/.*$/, "")
return pattern.test(code) ? [`${relative(root, file)}:${index + 1}: ${line.trim()}`] : []
}),
)
}
describe("a write never reloads to show itself", () => {
it("no module reloads the document", () => {
expect(offending(sources(modulesDir), RELOAD)).toEqual([])
})
// kit:notes:begin
it("finds the modules that declare live collections from their manifests", async () => {
// notes is the worked example; if it stops declaring one, the next test proves nothing about it.
expect(await modulesWithCollections()).toContain("notes")
})
// kit:notes:end
it("a module with a live collection re-runs no loader after a write", async () => {
const problems = (await modulesWithCollections()).flatMap((id) =>
offending(sources(join(modulesDir, id, "ui")), LOADER_RERUN),
)
expect(problems, "write with useWrite and read with useLive or a live useRead").toEqual([])
}, 120_000)
})
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Who may open which live stream (@conusai-kit/host/postgres `createHostLive`, umbrella TS6.2), decided with the
// kernel's own tenancy over this app's registry: a member gets the collection for a topic they
// resolve, with its ids; a signed-out request is 401; another organization's slug and a workspace
// the caller does not resolve are 404; an entity that is not a collection is denied, and so is a
// project collection asked for the whole organization; a member removed meanwhile is revoked at the
// next re-check, and one whose role lost the read permission of a granted collection is told
// `grants_changed`. The stream itself is proven by tests/live-conformance and e2e/notes-live.spec.ts.
import type { Principal } from "@conusai-kit/auth"
import { createHostLive, createLiveGrants } from "@conusai-kit/host/postgres"
import { createTenancyHooks, orgMemberships, TENANCY } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { makeApp, type Person } from "../modules/account/__tests__/harness.js"
const app = makeApp()
let principal: Principal | null = null
const quiet = {
log: { info: () => undefined, warn: () => undefined, error: () => undefined },
captureException: () => undefined,
span: <T>(_name: string, _attributes: unknown, fn: () => T): T => fn(),
}
const { authorize } = createHostLive({
runtime: () => ({ registry: app.registry, observe: quiet }),
requestContext: async () => {
const container = app.root.child()
container.value(TENANCY, createTenancyHooks({ data: app.data, registry: app.registry }))
return { principal, container }
},
log: () => undefined,
grants: createLiveGrants(),
})
const as = (person: Person | null) => {
principal = person ? ({ actor: person.actor, method: "bearer" } as Principal) : null
}
const open = (org: string, body: { entities: string[]; topics: Array<{ ws: string; project?: string }> }) =>
authorize(new Request(`http://app.test/api/v1/orgs/${org}/live`, { method: "POST" }), {
protocol: "clp/1",
after: null,
...body,
})
async function orgWithProject(person: Person, slug: string) {
const call = person.call()
const created = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string }
await call.callByName("tenancy.workspace.create", { org: slug, slug: "main", name: "Main", visibility: "org" })
await call.callByName("tenancy.project.create", {
org: slug,
workspace: "main",
slug: "notes",
name: "Notes",
moduleKey: "notes",
})
return created.orgId
}
describe("the live stream's authorization", async () => {
const ada = await app.signUp("ada-live@example.test")
const grace = await app.signUp("grace-live@example.test")
const acme = await orgWithProject(ada, "acme-live")
await orgWithProject(grace, "globex-live")
const topic = { ws: "main", project: "notes" }
it("grants a member the collection for a topic they resolve, with its ids, and denies the rest", async () => {
as(ada)
const grant = await open("acme-live", { entities: ["notes.note", "notes.secret"], topics: [topic] })
expect(grant.org).toBe(acme)
expect(grant.granted.entities).toEqual(["notes.note"])
expect(grant.granted.topics[0]?.ids.org).toBe(acme)
expect(grant.granted.topics[0]?.ids.ws).toBeTruthy()
expect(grant.granted.topics[0]?.ids.project).toBeTruthy()
expect(grant.denied).toEqual([{ entity: "notes.secret", reason: "unknown" }])
expect(await grant.recheck()).toBeNull()
})
it("denies a project collection for the whole organization", async () => {
as(ada)
const grant = await open("acme-live", { entities: ["notes.note"], topics: [] })
expect(grant.granted.entities).toEqual([])
expect(grant.denied).toEqual([{ entity: "notes.note", reason: "scope" }])
})
it("refuses a signed-out request, another organization's slug and a workspace the caller does not resolve", async () => {
as(null)
await expect(open("acme-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
status: 401,
})
as(grace)
await expect(open("acme-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
status: 404,
})
as(ada)
await expect(open("acme-live", { entities: ["notes.note"], topics: [{ ws: "nowhere" }] })).rejects.toMatchObject({
status: 404,
})
await expect(open("globex-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
status: 404,
})
})
it("ends a stream with grants_changed once the member's role no longer reads a granted collection", async () => {
const linus = await app.signUp("linus-live@example.test")
const invitation = (await ada.call().callByName("tenancy.member.invite", {
org: "acme-live",
email: linus.email,
role: "member",
})) as { token: string }
await linus.call().callByName("tenancy.invitation.accept", { org: "acme-live", token: invitation.token })
as(linus)
const grant = await open("acme-live", { entities: ["notes.note"], topics: [topic] })
expect(grant.granted.entities).toEqual(["notes.note"])
expect(await grant.recheck()).toBeNull()
// A custom organization role that reads the organization and nothing of the notes module.
const role = (await ada.call().callByName("tenancy.role.create", {
org: "acme-live",
name: "Directory only",
level: "org",
permissions: ["tenancy.org.read", "tenancy.workspace.read", "tenancy.project.read"],
})) as { key: string }
await ada.call().callByName("tenancy.member.assign", {
org: "acme-live",
userId: linus.actor.userId,
level: "org",
roleKey: role.key,
})
expect(await grant.recheck()).toBe("grants_changed")
})
it("revokes a member who was removed, at the next re-check", async () => {
as(ada)
const grant = await open("acme-live", { entities: ["notes.note"], topics: [topic] })
const scope = { level: "org" as const, orgId: acme, actor: ada.actor }
const rows = (await app.data.forScope(scope).of(orgMemberships).list({})).items
for (const row of rows) await app.data.forScope(scope).of(orgMemberships).delete(row.id)
expect(await grant.recheck()).toBe("membership")
})
})
+149
View File
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The kit's markers stay where the import sorter cannot move other code into them.
//
// `kit template:prune` removes every line that carries a `// kit:<name>` comment and every line between
// `// kit:<name>:begin` and `// kit:<name>:end`. Biome's import sorter (`organizeImports`) reorders an import
// section across comments and blank lines, and merges two imports of one source:
//
// - an import inside a begin/end block can trade places with an app's own import, which the prune then
// deletes without a word; so no block holds an import or a re-export;
// - a whole-line marker survives the sort only on a one-line import the sorter cannot merge: it merges two
// value imports (or two type-only imports, or two re-exports) of one source into one and keeps neither
// line's comment. A namespace import is never merged.
// A module's imports are one namespace import (`import * as widgets from "@app/modules/widgets" //
// kit:widgets`), or one marked name inside a shared import (`filesDeps, // kit:widgets`), which the
// sorter keeps with its name.
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
import { join, relative } from "node:path"
import ts from "typescript"
import { describe, expect, it } from "vitest"
const root = new URL("..", import.meta.url).pathname
const FOLDERS = ["apps", "config", "modules", "test", "e2e", "evals", "scripts"]
const SKIP = new Set(["node_modules", "dist", ".tanstack", "generated", ".logs"])
function sources(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name)
if (statSync(path).isDirectory()) return SKIP.has(name) ? [] : sources(path)
return /\.(?:ts|tsx|mts|cts|mjs|cjs|js)$/.test(name) && !name.endsWith(".gen.ts") ? [path] : []
})
}
const BEGIN = /\/\/\s*kit:([\w-]+):begin\b/
const END = /\/\/\s*kit:([\w-]+):end\b/
const LINE_MARK = /(?:\/\/|\/\*|\{\/\*)\s*kit:[\w-]+\b(?!:)/
interface Statement {
readonly first: number
readonly last: number
readonly source: string
/** What the sorter merges with what: value imports, type-only imports and re-exports, each among their own. */
readonly kind: "value" | "type" | "export"
/** `import * as x`: never merged with another import. */
readonly namespace: boolean
}
/** The file's static imports and `export … from`, with their 1-based first and last lines. */
function importsOf(file: string, text: string): Statement[] {
const parsed = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false)
const out: Statement[] = []
for (const node of parsed.statements) {
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node) ? node.moduleSpecifier : undefined
if (!specifier || !ts.isStringLiteral(specifier)) continue
const first = parsed.getLineAndCharacterOfPosition(node.getStart(parsed)).line + 1
const last = parsed.getLineAndCharacterOfPosition(node.getEnd()).line + 1
const clause = ts.isImportDeclaration(node) ? node.importClause : undefined
const kind = ts.isExportDeclaration(node) ? "export" : clause?.isTypeOnly ? "type" : "value"
const namespace = clause?.namedBindings !== undefined && ts.isNamespaceImport(clause.namedBindings)
out.push({ first, last, source: specifier.text, kind, namespace })
}
return out
}
/** Whether the import sorter would merge the two statements into one. */
const mergeable = (a: Statement, b: Statement): boolean =>
a !== b && a.source === b.source && a.kind === b.kind && !a.namespace && !b.namespace
/** `file:line: why` for each marker the import sorter can break. */
function markerProblems(file: string, text: string): string[] {
const lines = text.split("\n")
const imports = importsOf(file, text)
const where = (line: number) => `${file}:${line}`
const problems: string[] = []
const open: { name: string; line: number }[] = []
lines.forEach((line, index) => {
const begin = BEGIN.exec(line)
const end = END.exec(line)
if (begin) open.push({ name: begin[1] as string, line: index + 1 })
else if (end) {
const at = open.findLastIndex((block) => block.name === end[1])
if (at < 0) return
const [block] = open.splice(at, 1)
for (const statement of imports) {
if (block && statement.last >= block.line && statement.first <= index + 1) {
problems.push(
`${where(statement.first)}: an import of "${statement.source}" inside the kit:${block.name} block of line ${block.line}; the import sorter can move an app's own import into the block, and the prune would delete it`,
)
}
}
}
})
for (const statement of imports) {
for (let line = statement.first; line <= statement.last; line++) {
if (!LINE_MARK.test(lines[line - 1] ?? "")) continue
const multiLine = statement.first !== statement.last
if (multiLine && (line === statement.first || line === statement.last)) {
problems.push(
`${where(line)}: a marker on the ${line === statement.first ? "first" : "last"} line of a multi-line import of "${statement.source}"; mark the names inside it, or make it one line`,
)
} else if (!multiLine && !statement.namespace && imports.some((other) => mergeable(statement, other))) {
problems.push(
`${where(line)}: a marked import of "${statement.source}" beside another import of it; the import sorter merges them and drops the marker. Mark the names inside one import`,
)
}
}
}
return problems
}
describe("the kit's markers and the import sorter", () => {
it("no marker in the template's sources can be broken by sorting or merging imports", () => {
const problems = FOLDERS.flatMap((folder) => sources(join(root, folder))).flatMap((path) =>
markerProblems(relative(root, path), readFileSync(path, "utf8")),
)
expect(problems).toEqual([])
})
it("names an import inside a block, a marker on the edge of a multi-line import and a mergeable marked import", () => {
const text = [
'import { a } from "@app/modules/account"',
"// kit:widgets:begin",
'import { w } from "@app/modules/widgets"',
"// kit:widgets:end",
"import {",
" d,",
'} from "@app/modules/gadgets" // kit:gadgets',
'import { f1 } from "@conusai-kit/files"',
'import { f2 } from "@conusai-kit/files" // kit:widgets',
"import {",
" g1, // kit:widgets",
" g2,",
'} from "@conusai-kit/data"',
'import * as gizmos from "@conusai-kit/gizmos" // kit:gizmos',
'import { s } from "@conusai-kit/gizmos"',
'import type { T } from "@conusai-kit/auth"',
'import { load } from "@conusai-kit/auth" // kit:sprockets',
"export const all = [a, w, d, f1, f2, g1, g2, gizmos, s, load]",
].join("\n")
expect(markerProblems("x.ts", text).map((problem) => problem.split(": ")[0])).toEqual([
"x.ts:3",
"x.ts:7",
"x.ts:9",
])
})
})
+60
View File
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The tools this app opts in to, listed by the real mount over the real registry through the official
// SDK client. The transport's own rules are `@conusai-kit/host/http`'s tests.
import { buildRegistry } from "@conusai-kit/core"
import { createMcpHandler } from "@conusai-kit/host/http"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { describe, expect, it, vi } from "vitest"
import { appModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
describe("this app's MCP tools", () => {
it("are the procedures that opted in, and a tool call goes through the scoped caller", async () => {
const output = { notes: [], hasMore: false }
const callByName = vi.fn(async () => output)
const handle = createMcpHandler({
registry,
contextFor: async () => ({
principal: { method: "bearer", actor: { userId: "user-a" } },
caller: { callByName } as never,
}),
checkCsrf: () => undefined,
})
const transport = new StreamableHTTPClientTransport(new URL("https://app.example.test/api/mcp"), {
fetch: (url, init) => handle(new Request(url, init), "sdk-request"),
})
const client = new Client({ name: "app-template-test", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } })
try {
await client.connect(transport)
const listed = await client.listTools()
// The Agent's workspace reads are tools too: what an MCP client of a workspace can list and open.
expect(listed.tools.map((tool) => tool.name)).toEqual([
"agent.run.status", // kit:agent
"assistant.workspace.openProject", // kit:agent
"assistant.workspace.projects", // kit:agent
"knowledge.search", // kit:knowledge
"notes.note.list",
])
expect(listed.tools.find((tool) => tool.name === "notes.note.list")?.inputSchema.required).toEqual([
"org",
"workspace",
"project",
])
const result = await client.callTool({
name: "notes.note.list",
arguments: { org: "org-a", workspace: "ws-a", project: "project-a" },
})
expect(result.structuredContent).toEqual(output)
} finally {
await client.close()
}
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["agent", "knowledge"]),
}))
+145
View File
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What the migrate job applies, and the one grant list the global reader gets (decision AT-D20).
import { readdirSync, readFileSync } from "node:fs"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { type Migration, OUTBOX_READER_GRANT } from "@conusai-kit/data-kysely"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { describe, expect, it } from "vitest"
import { CROSS_TENANT_READS, jobSchemaGrants, readerGrants, splitStatements } from "../apps/worker/src/migrations.js"
import { appModules } from "../modules/index.js"
const dir = new URL("../migrations/postgres/", import.meta.url)
const files = readdirSync(dir)
.filter((name) => name.endsWith(".sql"))
.sort()
const committed = files.map((name) => readFileSync(new URL(name, dir), "utf8")).join("\n")
const registry = buildRegistry([...appModules])
describe("the committed migrations", () => {
it("are numbered, forward only, and start at 0001", () => {
expect(files.length).toBeGreaterThan(0)
expect(files[0]).toMatch(/^0001_/)
for (const name of files) expect(name).toMatch(/^\d{4}_[a-z0-9-]+\.sql$/)
})
it("create a table for every entity this app's registry declares", () => {
const declared = [...entitiesOf(registry, "postgres"), ...KIT_JOBS_ENTITIES]
const inModules = new Set(registry.migrations.postgres.map(({ module }) => module))
for (const entity of declared) {
// A module that ships its own migration folder is allowed to own its tables there.
if (inModules.size > 0 && !committed.includes(`create table "${entity.table}"`)) continue
expect(committed, `no migration creates ${entity.table}`).toContain(`create table "${entity.table}"`)
}
})
it("split into statements without losing a dollar-quoted body", () => {
const statements = splitStatements('create table "a" ("x" int);\n\ndo $$ begin perform 1; end $$;\n')
expect(statements).toHaveLength(2)
expect(statements[1]).toContain("do $$ begin perform 1; end $$")
})
})
describe("the Agent's tables", () => {
it("are the kit's, made again for the workspace-level Agent with no conversation carried over", () => {
// The kit's file, as the package that depends on it installed it.
const kitFile = new URL("../modules/node_modules/@conusai-kit/agent/migrations/0001_assistant.sql", import.meta.url)
const kit = readFileSync(kitFile, "utf8").split("\n").slice(1).join("\n")
const app = readFileSync(
new URL("../modules/agent/migrations/0005_assistant_workspace.sql", import.meta.url),
"utf8",
)
expect(app.endsWith(kit)).toBe(true)
for (const table of ["assistant_receipt", "assistant_thread", "assistant_turn", "assistant_conversation"])
expect(app).toContain(`drop table "${table}";`)
expect(app).toMatch(/create table "assistant_conversation" \([^;]*"project_id" uuid\n\);/)
})
})
describe("the global reader's grants", () => {
const [migration, workspaces] = readerGrants("postgresql://app_reader:secret@db:5432/app") as [Migration, Migration]
const sql = migration.statements.join(";\n")
const tables = ["outbox", "org_memberships", "projects", "auth_api_keys"]
it("grant the cross-tenant reads the kit modules declare, the outbox, and no other table", () => {
// The list is the modules' own declarations, so adding a read to a module adds the grant here
// and a table nobody declared is never granted.
expect(CROSS_TENANT_READS.map((read) => read.entity.table).sort()).toEqual([
"auth_api_keys",
"org_memberships",
"projects",
"workspaces",
])
const granted = migration.statements.filter((statement) => statement.startsWith("grant select"))
expect(granted).toHaveLength(tables.length)
for (const table of tables) expect(sql).toContain(`on "${table}" to "app_reader"`)
})
it("keep the text an installed app recorded: the runner refuses a migration that changed", () => {
// Literal on purpose. A read names its columns, so a column added to an entity changes nothing
// here. A column the reader really needs is a new migration, never an edit of this list.
const granted = (table: string) =>
migration.statements.find(
(statement) => statement.startsWith("grant select") && statement.includes(` on "${table}" `),
)
expect(granted("org_memberships")).toBe(
'grant select ("id", "version", "created_at", "updated_at", "org_id", "user_id", "role", "role_key") on "org_memberships" to "app_reader"',
)
expect(granted("projects")).toBe(
'grant select ("id", "version", "created_at", "updated_at", "org_id", "workspace_id", "slug", "name", "module_key", "archived_at", "visibility") on "projects" to "app_reader"',
)
expect(granted("auth_api_keys")).toBe(
'grant select ("id", "version", "created_at", "updated_at", "org_id", "name", "token_hash", "prefix", "user_id", "expires_at", "revoked_at") on "auth_api_keys" to "app_reader"',
)
})
it("grant a read added later by a migration of its own, so the first keeps the text it shipped with", () => {
expect(migration.name).toBe("0002_reader_grants")
expect(workspaces.name).toBe("0004_reader_grants_workspaces")
expect(workspaces.statements).toEqual([
'grant select ("id", "version", "created_at", "updated_at", "org_id", "slug", "name", "is_default", "visibility") on "workspaces" to "app_reader"',
'create policy "workspaces_global_reader" on "workspaces" for select to "app_reader" using (true)',
])
})
it("never reach a project's description, which is a person's free text", () => {
expect(sql).not.toContain('"description"')
})
it("let the reader write nothing but the columns the relay keeps its books in", () => {
// A grant on the whole row would let the reader rewrite an event's organization, name or
// payload, which the relay then hands to a subscriber running as a system actor.
const update = migration.statements.filter((statement) => statement.startsWith("grant update"))
expect(update).toHaveLength(1)
expect(update[0]).toContain('on "outbox" to "app_reader"')
for (const column of OUTBOX_READER_GRANT.update ?? []) expect(update[0]).toContain(`"${column}"`)
expect(update[0]).not.toContain('"payload"')
expect(update[0]).not.toContain('"org_id"')
expect(update[0]).not.toContain('"name"')
expect(sql).not.toMatch(/grant [^;]*insert/)
expect(sql).not.toMatch(/grant [^;]*delete/)
})
it("give the reader a policy of its own, because row-level security binds it too", () => {
// The reader owns nothing and has NOBYPASSRLS (data-services DS21), so without a policy
// naming it the forced policy on app.org_id would answer zero rows for every read.
for (const table of tables) {
expect(sql).toContain(
`create policy "${table}_global_reader" on "${table}" for select to "app_reader" using (true)`,
)
}
expect(sql).toContain('create policy "outbox_global_reader_update"')
})
it("name the role from the reader's own URL, quoted", () => {
expect(readerGrants("postgresql://other_reader:s@db:5432/app")[0]?.statements[0]).toContain('"other_reader"')
})
it("grant the runtime role the job schema a transactional enqueue needs", () => {
const jobs = jobSchemaGrants("postgresql://app_app:secret@db:5432/app").statements.join(";\n")
expect(jobs).toContain('grant usage on schema "pgboss" to "app_app"')
expect(jobs).toContain('alter default privileges in schema "pgboss"')
expect(jobs).not.toContain("app_reader")
})
})
+162
View File
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { readFileSync } from "node:fs"
import { buildRegistry, type ModuleManifest, type Token } from "@conusai-kit/core"
import { generateConvex } from "@conusai-kit/data-convex"
import { guidesModule } from "@conusai-kit/guides" // kit:guides
import { supportModule } from "@conusai-kit/support" // kit:support
import { accountDataOf } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { composedClientModules } from "../apps/web/src/ui/client-module-list.js"
import { helpFor } from "../apps/web/src/ui/help.js"
import { convexModules } from "../convex/lib/convexModules.mjs"
import { agentModule, assistantAppModule } from "../modules/agent/index.js" // kit:agent
import { custodyModule } from "../modules/custody.js"
import { documentsModule } from "../modules/documents/index.js" // kit:documents
import { moduleInclusion, OPTIONAL_MODULE_IDS, validateInclusion } from "../modules/inclusion.js"
import { appModules } from "../modules/index.js"
import { knowledgeModule } from "../modules/knowledge/index.js" // kit:knowledge
import { modulePages } from "../modules/pages.js"
describe("fresh module admission", () => {
it.runIf(moduleInclusion.everAdopted.length === 0)(
"registers no optional procedure, job, permission, page or rights obligation",
() => {
expect(moduleInclusion).toEqual({ schemaVersion: 1, selected: [], everAdopted: [] })
const config = JSON.parse(readFileSync(new URL("../conusai.json", import.meta.url), "utf8"))
expect(
config.token.permissions.filter((scope: string) =>
/^(ocr:|ai:|ai_tools:|knowledge[.:]|guides:|support:)/.test(scope),
),
).toEqual([])
const registry = buildRegistry([...appModules])
expect(appModules.some((m) => OPTIONAL_MODULE_IDS.some((id) => id === m.id) || m.id === "assistant")).toBe(false)
expect(Object.keys(modulePages)).toEqual(["notes"])
expect(accountDataOf(registry).some((share) => ["guides", "support", "assistant"].includes(share.key))).toBe(
false,
)
},
)
it.runIf(moduleInclusion.everAdopted.length === 0)("mounts no optional help section/client", () => {
expect(
helpFor({ locale: "en" } as never, { title: "Help", sections: "Sections", guides: "Guides", support: "Support" })
.any,
).toBe(false)
expect(composedClientModules.modules).toEqual([])
expect(composedClientModules.providers).toEqual([])
expect(composedClientModules.slots).toEqual([])
})
it("refuses unbounded fields, IDs, versions and selected-without-custody", () => {
for (const value of [
{ schemaVersion: 2, selected: [], everAdopted: [] },
{ schemaVersion: 1, selected: ["agent"], everAdopted: [] },
{ schemaVersion: 1, selected: [], everAdopted: ["customer"] },
{ schemaVersion: 1, selected: [], everAdopted: [], subject: "synthetic" },
])
expect(() => validateInclusion(value)).toThrow()
})
})
describe("retained manifests", () => {
it.each<ModuleManifest>([
documentsModule, // kit:documents
knowledgeModule, // kit:knowledge
agentModule, // kit:agent
assistantAppModule, // kit:agent
guidesModule, // kit:guides
supportModule, // kit:support
])("retains $id schema under its original identity without interactive admission", (original) => {
const retained = custodyModule(original)
expect(retained.id).toBe(original.id)
expect(retained.repositories).toBe(original.repositories)
expect(retained.migrations).toBe(original.migrations)
for (const key of [
"procedures",
"permissions",
"collections",
"ui",
"serviceRequirements",
"extensionPoints",
] as const)
expect(retained[key]).toBeUndefined()
expect((retained.extends ?? []).every((entry) => entry.point.id === "tenancy.account")).toBe(true)
})
it("retains exactly the original rights hooks and retention/removal jobs", () => {
const withRights: ModuleManifest[] = [
guidesModule, // kit:guides
supportModule, // kit:support
assistantAppModule, // kit:agent
]
for (const manifest of withRights)
expect(custodyModule(manifest).extends).toEqual(
manifest.extends?.filter((entry) => entry.point.id === "tenancy.account"),
)
// kit:agent:begin
expect(Object.keys(custodyModule(agentModule).jobs ?? {})).toEqual(["agent.runs.expire"])
expect(Object.keys(custodyModule(assistantAppModule).jobs ?? {})).toEqual(["assistant.conversations.expire"])
const schedules = custodyModule(agentModule).schedules
expect(typeof schedules === "function" ? schedules({}) : []).toHaveLength(1)
// kit:agent:end
// kit:knowledge:begin
const retained = custodyModule(knowledgeModule)
expect(Object.keys(retained.jobs ?? {})).toEqual(["knowledge.remove"])
expect(retained.events?.subscribes?.map((entry) => entry.event.name)).toEqual([
"files.FileDeleted",
"knowledge.documentRemoveRequested",
])
// kit:knowledge:end
})
})
it.runIf(moduleInclusion.everAdopted.length === 0)(
"does not construct optional provider ports on a fresh web/worker boot",
async () => {
const { Container } = await import("@conusai-kit/core")
const { bindWebPorts } = await import("../apps/web/src/server/ports.js")
const { bindModulePorts } = await import("../apps/worker/src/ports.js")
const optionalPorts: Token<unknown>[] = [
(await import("../modules/documents/index.js")).DOCUMENT_OCR, // kit:documents
(await import("@conusai-kit/guides")).GUIDES_PLATFORM, // kit:guides
(await import("@conusai-kit/support")).SUPPORT_PLATFORM, // kit:support
(await import("../modules/knowledge/index.js")).KNOWLEDGE_SEARCH, // kit:knowledge
]
const platform = {
mode: "fake",
deployment: { CONUSAI_API_URL: "http://127.0.0.1:1/api", CONUSAI_API_TOKEN: "synthetic" },
objectStore: {},
audit: {},
usage: {},
}
for (const key of ["models", "documents", "model"])
Object.defineProperty(platform, key, {
get: () => {
throw new Error("optional provider constructed at boot")
},
})
const registry = buildRegistry([...appModules])
for (const bind of [bindWebPorts, bindModulePorts]) {
const container = new Container()
bind(container, platform as never, registry)
for (const token of optionalPorts) expect(container.has(token)).toBe(false)
}
},
)
describe("committed Convex projection", () => {
const root = new URL("..", import.meta.url)
it("commits generated wrappers that forward explicit idempotency keys to the kernel", () => {
// The same profile filter as below: the Postgres-only modules are not Convex-deployed yet.
const convexBound = appModules.filter(
(module) => module.profiles === undefined || module.profiles.includes("convex"),
)
const registry = buildRegistry([...convexModules(convexBound)])
const generated = generateConvex(registry, {
header: "// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n",
})
for (const file of generated) {
expect(readFileSync(new URL(`convex/${file.path}`, root), "utf8"), file.path).toBe(file.content)
}
const proposal = generated.find(({ path }) => path === "generated/proposals.ts")
expect(proposal?.content).toContain(
'ctx.run("proposals.proposal.create", input, idempotencyKey === undefined ? undefined : { idempotencyKey })',
)
})
})
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The isolation suite reads the fixtures of registered modules only (template lesson core-2): a module folder
// kept in the repository but not registered must not fail it.
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, expect, it } from "vitest"
import { isolationFilesOf } from "./support/module-isolations.js"
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function modulesWith(layout: Record<string, boolean>): string {
const dir = mkdtempSync(join(tmpdir(), "isolation-modules-"))
made.push(dir)
for (const [id, hasFixtures] of Object.entries(layout)) {
mkdirSync(join(dir, id, "__tests__"), { recursive: true })
if (hasFixtures) writeFileSync(join(dir, id, "__tests__", "isolation.ts"), "export const isolation = {}\n")
}
return dir
}
it("reads the fixtures of the registered modules and skips a held module's", () => {
const dir = modulesWith({ widgets: true, held: true, plain: false })
expect(isolationFilesOf(dir, ["widgets", "plain"])).toEqual([join(dir, "widgets", "__tests__", "isolation.ts")])
})
+239
View File
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Every table of a module the kit ships has a committed, forward-only migration, and each is exactly
// `schemaStatements()` over that module's own entities (decision AT-D20: the entity definitions are
// the one source of a column, so a hand-written column cannot drift from the schema a module validates
// its rows with). A kit upgrade that changes a table changes a snapshot here, which is the moment a
// customer must see a new migration in review.
//
// `0002` and `0003` are the grants the migrate job builds at run time (apps/worker/src/migrations.ts),
// so the files of the kit's modules take the next numbers.
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { appRoot, moduleMigrationFolder } from "@app/config"
import { ACCOUNT_ENTITIES, accountModule } from "@app/modules/account"
import { DOCUMENTS_ENTITIES, documentsModule } from "@app/modules/documents" // kit:documents
import { authModule } from "@conusai-kit/auth"
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { mysqlSchemaStatements } from "@conusai-kit/data-kysely/mysql"
import { MONGO_KERNEL_COMMANDS, mongoSchemaCommands } from "@conusai-kit/data-mongo"
import { filesModule } from "@conusai-kit/files"
import { liveSchemaStatements } from "@conusai-kit/live-server"
import { proposalsModule } from "@conusai-kit/proposals"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { SPDX_LINE } from "../scripts/licence.mjs"
/** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for a module's MariaDB folder (spec §5.3: one numbered SQL folder per dialect). */
function mysqlTablesOf(entities: Parameters<typeof mysqlSchemaStatements>[0]): string {
const kernel = mysqlSchemaStatements([]).length
return `${mysqlSchemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
type MongoCommands = ReturnType<typeof mongoSchemaCommands>
/** The same for a module's MongoDB folder: `mongoSchemaCommands` leads with the kernel's own collections. */
function mongoCommandsOf(entities: Parameters<typeof mongoSchemaCommands>[0]): MongoCommands {
return mongoSchemaCommands(entities).slice(MONGO_KERNEL_COMMANDS.length)
}
/** A file of the account module, which the package ships: the folder is the one the migrate job reads. */
const accountMigration = (profile: "mariadb" | "mongodb", name: string): string => {
const path = accountModule.migrations?.[profile] ?? ""
return readFileSync(join(moduleMigrationFolder({ module: "account", path }, appRoot()), name), "utf8")
}
// kit:documents:begin
/**
* A validator as the generator wrote it before a nullable enum listed null: the files that ran then stay what
* they were, and a later file carries the current validator.
*/
const withoutListedNull = <T>(value: T): T =>
JSON.parse(JSON.stringify(value, (_, item) => (Array.isArray(item) ? item.filter((entry) => entry !== null) : item)))
// kit:documents:end
const entitiesOfModule = (module: Parameters<typeof buildRegistry>[0][number]) =>
entitiesOf(buildRegistry([tenancyModule, module]), "postgres").filter(
(entity) => !entitiesOf(buildRegistry([tenancyModule]), "postgres").some((own) => own.table === entity.table),
)
describe("the account module's dialect migrations (data-subject rights on every profile)", () => {
// First shipped with both columns 0003 added on Postgres (steps, provider), so each dialect's 0001
// is the ledger and the revocation outbox as they stood then; what `identity.erase` added since
// (identity_subjects, identity_note, and the `held` state) is each dialect's 0002: a migration that
// has run is never edited, and the files together are exactly the entities.
it("ships the MariaDB tables as mysqlSchemaStatements generates them", () => {
const mysqlMigration = (name: string) => accountMigration("mariadb", name)
const added = ",\n `identity_subjects` json,\n `identity_note` text\n"
const generated = mysqlTablesOf(ACCOUNT_ENTITIES)
expect(generated).toContain(added)
expect(generated.replace(added, "\n")).toBe(mysqlMigration("0001_account.sql"))
expect(mysqlMigration("0002_identity_erasure.sql")).toBe(
"alter table `account_deletion` add column `identity_subjects` json;\n\nalter table `account_deletion` add column `identity_note` text;\n",
)
})
it("ships the MongoDB collections as mongoSchemaCommands generates them", () => {
// Compared as data: the committed file is formatted by Biome, the generator's output is not.
const mongoMigration = (name: string): MongoCommands => JSON.parse(accountMigration("mongodb", name))
const current = mongoCommandsOf(ACCOUNT_ENTITIES)
const create = current.find(
(command) => "createCollection" in command && command.createCollection === "account_deletion",
)
if (!create || !("createCollection" in create)) throw new Error("no createCollection for account_deletion")
const first = mongoMigration("0001_account.json")
expect(JSON.stringify(first)).not.toContain("identity_subjects")
expect(
first.filter((command) => !("createCollection" in command && command.createCollection === "account_deletion")),
).toEqual(current.filter((command) => command !== create))
const shipped = structuredClone(create.validator)
const fields = shipped.$jsonSchema.properties as Record<string, unknown>
fields.identity_note = { bsonType: ["string", "null"] }
expect(mongoMigration("0002_identity_erasure.json")).toEqual([{ collMod: "account_deletion", validator: shipped }])
expect(mongoMigration("0003_identity_note_enum.json")).toEqual([
{ collMod: "account_deletion", validator: create.validator },
])
})
it("is declared by the manifest for every profile, so the migrate job finds it", () => {
expect(accountModule.migrations).toEqual({
postgres: "@conusai-kit/account/migrations/postgres",
mariadb: "@conusai-kit/account/migrations/mariadb",
mongodb: "@conusai-kit/account/migrations/mongodb",
})
for (const profile of ["postgres", "mariadb", "mongodb"] as const) {
expect(
entitiesOf(buildRegistry([tenancyModule, authModule, accountModule]), profile).map(({ table }) => table),
).toEqual(expect.arrayContaining(["account_deletion", "account_session_revocation"]))
}
})
})
describe("the live change log (umbrella TS6.2)", () => {
it("0007_live.sql is @conusai-kit/live-server's schema: the log, its policies, the capture and the prune", async () => {
const sql = `${liveSchemaStatements()
.map((statement) => `${statement};`)
.join("\n\n")}\n`
await expect(`-- ${SPDX_LINE}\n${sql}`).toMatchFileSnapshot("../migrations/postgres/0007_live.sql")
})
})
describe("the migrations of the modules the kit ships", () => {
it("0004_files.sql creates the files table", async () => {
await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(filesModule))}`).toMatchFileSnapshot(
"../migrations/postgres/0004_files.sql",
)
})
it("0005_proposals.sql creates the proposals table", async () => {
await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(proposalsModule))}`).toMatchFileSnapshot(
"../migrations/postgres/0005_proposals.sql",
)
})
})
// kit:documents:begin
describe("the migration of the documents module", () => {
const migration = (name: string) =>
readFileSync(new URL(`../modules/documents/migrations/${name}`, import.meta.url), "utf8")
// A migration that has run is never edited, so a column added later is a file of its own: the first
// file stays what it was, the second adds the column, and together they are exactly the entities.
it("0001 is its two tables as first shipped, and 0002 adds the one column since", () => {
expect(migration("0002_skipped_pages.sql")).toBe(
'alter table "documents_document" add column "skipped_pages" jsonb;\n',
)
const generated = tablesOf(DOCUMENTS_ENTITIES)
expect(generated).toContain(' "skipped_pages" jsonb,\n')
expect(generated.replace(' "skipped_pages" jsonb,\n', "")).toBe(migration("0001_documents.sql"))
})
it("is declared by the manifest, so the migrate job finds it", () => {
expect(documentsModule.migrations).toEqual({
postgres: "migrations",
mariadb: "adapters/kysely/mysql/migrations",
mongodb: "adapters/mongo/migrations",
})
const sql = migration("0001_documents.sql")
for (const table of ["documents_document", "documents_line"]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
it("ships the same migrations in the MariaDB dialect (AT3.1)", () => {
const mysqlMigration = (name: string) =>
readFileSync(new URL(`../modules/documents/adapters/kysely/mysql/migrations/${name}`, import.meta.url), "utf8")
expect(mysqlMigration("0002_skipped_pages.sql")).toBe(
"alter table `documents_document` add column `skipped_pages` json;\n",
)
const generated = mysqlTablesOf(DOCUMENTS_ENTITIES)
expect(generated).toContain(" `skipped_pages` json,\n")
expect(generated.replace(" `skipped_pages` json,\n", "")).toBe(mysqlMigration("0001_documents.sql"))
})
it("ships the same migrations as MongoDB commands: 0001 the collections as first shipped, 0002 the added field (AT3.2)", () => {
const mongoMigration = (name: string): MongoCommands =>
JSON.parse(
readFileSync(new URL(`../modules/documents/adapters/mongo/migrations/${name}`, import.meta.url), "utf8"),
)
const current = mongoCommandsOf(DOCUMENTS_ENTITIES)
const createDocument = current.find(
(command) => "createCollection" in command && command.createCollection === "documents_document",
)
if (!createDocument || !("createCollection" in createDocument))
throw new Error("no createCollection for documents_document")
// A migration that has run is never edited, so a field added later is a collMod of its own:
// 0001/0002 predate processing_exhausted; 0003 adds null, and 0004 carries the current model.
const first = mongoMigration("0001_documents.json")
const created = first.find(
(command) => "createCollection" in command && command.createCollection === "documents_document",
)
if (!created || !("createCollection" in created)) throw new Error("0001 does not create documents_document")
expect(JSON.stringify(created.validator)).not.toContain("skipped_pages")
const historical = structuredClone(createDocument)
const failure = historical.validator.$jsonSchema.properties as { failure_code: { enum: unknown[] } }
failure.failure_code.enum = failure.failure_code.enum.filter((code) => code !== "processing_exhausted")
const withoutSkippedPages = withoutListedNull(historical)
const properties = withoutSkippedPages.validator.$jsonSchema.properties as Record<string, unknown>
expect(JSON.stringify(properties)).toContain("skipped_pages")
delete properties.skipped_pages
expect(JSON.stringify(created.validator)).toBe(JSON.stringify(withoutSkippedPages.validator))
// The other collection and every index are exactly the generated ones.
const rest = (commands: MongoCommands) =>
JSON.stringify(
commands.filter(
(command) => !("createCollection" in command && command.createCollection === "documents_document"),
),
)
expect(rest(first)).toBe(rest(current))
const second = mongoMigration("0002_skipped_pages.json")
expect(second).toEqual([{ collMod: "documents_document", validator: withoutListedNull(historical.validator) }])
// The third lets a nullable failure code be null, which the validators before it refused.
expect(mongoMigration("0003_nullable_failure_code.json")).toEqual([
{ collMod: "documents_document", validator: historical.validator },
])
// The latest cumulative validator, rather than every historical version, matches today's model.
expect(mongoMigration("0004_processing_exhausted.json")).toEqual([
{ collMod: "documents_document", validator: createDocument.validator },
])
})
})
// kit:documents:end
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A business module is its service: every procedure is a call to it, so the module runs only on the profiles
// its business requirement names, and its manifest says so in `profiles`, which is what the Convex generator
// and the profile tests read (template lesson core-17). Without it, CRM was projected into the Convex backend
// once an app selected it. A native requirement (OCR, AI) is different: the module degrades without it, and
// `documents` deploys to Convex on purpose.
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import type { ModuleManifest } from "@conusai-kit/core"
import { expect, it } from "vitest"
const modulesDir = join(import.meta.dirname, "..", "modules")
const moduleIds = readdirSync(modulesDir).filter((name) => existsSync(join(modulesDir, name, "index.ts")))
const isManifest = (value: unknown): value is ModuleManifest =>
typeof value === "object" && value !== null && "id" in value && "procedures" in value
it("a business module claims no profile its service does not run on", async () => {
const problems: string[] = []
for (const id of moduleIds) {
const exports = (await import(join(modulesDir, id, "index.ts"))) as Record<string, unknown>
for (const manifest of Object.values(exports).filter(isManifest)) {
for (const requirement of manifest.serviceRequirements ?? []) {
if (requirement.transport !== "business" || requirement.profiles === undefined) continue
const supported = new Set<string>(requirement.profiles)
const claimed = manifest.profiles ?? ["every profile"]
const extra = claimed.filter((profile) => !supported.has(profile))
if (extra.length > 0) {
problems.push(
`${manifest.id}: claims ${extra.join(", ")}; ${requirement.service} runs on ${requirement.profiles.join(", ")}`,
)
}
}
}
}
expect(problems).toEqual([])
}, 120_000)
+35
View File
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Module cron comes from the manifests (TM-R3): a module declares `schedules` beside its `jobs`, and
// `apps/worker/src/schedules.ts` names no module, so adding one edits nothing under apps/.
import { readFileSync } from "node:fs"
import { appModules } from "@app/modules"
import { buildRegistry, defineModule } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import { z } from "zod"
import { appSchedules } from "../apps/worker/src/schedules.js"
describe("the worker's module cron", () => {
it("is every schedule the manifests declare, each with its module's id", () => {
const schedules = appSchedules({})
expect(schedules.map((schedule) => schedule.module)).toEqual(expect.arrayContaining(["files", "account"]))
const registry = buildRegistry([...appModules])
for (const schedule of schedules) expect(registry.jobs[schedule.job]?.module).toBe(schedule.module)
})
it("names no module in the scaffold's file", () => {
const source = readFileSync(new URL("../apps/worker/src/schedules.ts", import.meta.url), "utf8")
expect(source).not.toMatch(/@app\/modules\/|_SCHEDULES|defineSchedules/)
})
it("refuses a schedule of a job the module does not declare, at boot", () => {
const module = defineModule({
id: "reports",
version: "0.1.0",
kit: "^0.1.0",
jobs: { "reports.weekly": { payload: z.null(), handler: async () => undefined } },
schedules: [{ job: "reports.nightly", cron: "0 2 * * *", scope: null }],
})
expect(() => buildRegistry([module])).toThrow(/reports\.nightly/)
})
})
+35
View File
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The worker admits a job that leaves the process only for the person it acts for (apps/worker/src/
// native-admission.ts): the job's intent names the permissions that person must still hold. A module
// job that declares a platform operation and has no intent is refused every time it runs, so every
// such job has one.
import * as documents from "@app/modules/documents" // kit:documents
import * as knowledge from "@app/modules/knowledge" // kit:knowledge
import type { ModuleManifest } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import { JOB_INTENT_PERMISSIONS } from "../apps/worker/src/native-admission.js"
const manifests: ModuleManifest[] = [
documents.documentsModule, // kit:documents
knowledge.knowledgeModule, // kit:knowledge
]
describe("the worker's native job admission", () => {
it("has an intent for every module job that declares a platform operation", () => {
for (const manifest of manifests) {
const jobs = Object.keys(manifest.jobs ?? {})
const declared = (manifest.serviceRequirements ?? [])
.filter((requirement) => requirement.transport === "platform")
.flatMap((requirement) => Object.keys(requirement.operations))
.filter((operation) => jobs.includes(operation))
for (const job of declared) expect(JOB_INTENT_PERMISSIONS[job]?.length, job).toBeGreaterThan(0)
}
})
// kit:knowledge:begin
it("admits the automatic PDF intake only while its administrator may still manage knowledge", () => {
expect(JOB_INTENT_PERMISSIONS[knowledge.AUTO_INDEX_JOB]).toEqual(["knowledge.knowledge.manage"])
})
// kit:knowledge:end
})
+366
View File
@@ -0,0 +1,366 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The notes module through the kernel's caller, the files module, the outbox relay's behaviour and
// the worker's job runner: what is proven is the wiring and not a unit. The three things its plan item
// asks for are here by name -- an upload records one usage event and one audit event, the flag
// switches attachments off, and a tenant's notes and files are its own.
import { MAX_ATTACHMENTS, NOTES_CREATED_METER, type NotesFlags, type NoteView } from "@app/modules/notes"
import { AppError, Container } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import { FILES_AUDIT, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { NOTIFICATIONS_REPOS } from "@conusai-kit/notifications"
import { uploadViaProcedures } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { bytesOf, makeNotesApp, type NotesApp, type SeededProject } from "./support/notes-app.js"
interface Detail {
note: NoteView
attachments: Array<{ attachmentId: string; fileId: string; filename: string; contentType: string; size: number }>
attachmentsEnabled: boolean
}
async function fresh(flags?: NotesFlags): Promise<{ app: NotesApp; project: SeededProject }> {
const app = makeNotesApp(flags ? { flags } : {})
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
const run = (project: SeededProject, procedure: string, input: Record<string, unknown> = {}) =>
project.call.callByName(procedure, { ...project.slugs, ...input })
const create = async (project: SeededProject, title = "Kick-off", body = "Agenda") =>
(await run(project, "notes.note.create", { title, body })) as NoteView
const detail = async (project: SeededProject, noteId: string) =>
(await run(project, "notes.note.get", { noteId })) as Detail
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
describe("notes", () => {
it("creates, lists, reads, edits and deletes a note", async () => {
const { project } = await fresh()
const note = await create(project)
expect(note).toMatchObject({ title: "Kick-off", body: "Agenda", version: 1, attachmentCount: 0 })
const listed = (await run(project, "notes.note.list")) as { notes: NoteView[] }
expect(listed.notes.map((entry) => entry.noteId)).toEqual([note.noteId])
const edited = (await run(project, "notes.note.update", {
noteId: note.noteId,
title: "Kick-off call",
body: "Agenda and owners",
expectedVersion: note.version,
})) as NoteView
expect(edited).toMatchObject({ title: "Kick-off call", version: 2 })
expect((await detail(project, note.noteId)).note.body).toBe("Agenda and owners")
await run(project, "notes.note.delete", { noteId: note.noteId })
expect(((await run(project, "notes.note.list")) as { notes: NoteView[] }).notes).toEqual([])
expect((await refusal(run(project, "notes.note.get", { noteId: note.noteId }))).status).toBe(404)
})
it("refuses a second editor's stale save with 409, so nothing is overwritten silently", async () => {
const { project } = await fresh()
const note = await create(project)
await run(project, "notes.note.update", { noteId: note.noteId, title: "A", body: "", expectedVersion: 1 })
const stale = await refusal(
run(project, "notes.note.update", { noteId: note.noteId, title: "B", body: "", expectedVersion: 1 }),
)
expect(stale.status).toBe(409)
expect((await detail(project, note.noteId)).note.title).toBe("A")
})
it("lets a viewer read and nothing else, and answers a write as the kernel answers a stranger: 404", async () => {
const { app, project } = await fresh()
const note = await create(project)
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const asViewer = (procedure: string, input: Record<string, unknown>) =>
viewer.call.callByName(procedure, { ...project.slugs, ...input })
expect(((await asViewer("notes.note.list", {})) as { notes: unknown[] }).notes).toHaveLength(1)
expect((await refusal(asViewer("notes.note.create", { title: "x", body: "" }))).status).toBe(404)
expect(
(await refusal(asViewer("notes.note.update", { noteId: note.noteId, title: "x", body: "", expectedVersion: 1 })))
.status,
).toBe(404)
expect((await refusal(asViewer("notes.note.delete", { noteId: note.noteId }))).status).toBe(404)
})
})
describe("what a note records, exactly once", () => {
it("writes one audit event and one usage fact per note created, and none of its text", async () => {
const { app, project } = await fresh()
const note = await create(project, "Secret plan", "Body text")
expect(app.audit.events).toEqual([])
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["notes.note_created"])
expect(app.audit.events[0]).toMatchObject({
targetId: `${project.orgId}/${note.noteId}`,
actorId: project.owner.userId,
})
expect(JSON.stringify(app.audit.events)).not.toContain("Secret plan")
expect(JSON.stringify(app.audit.events)).not.toContain("Body text")
expect(app.usage.facts).toEqual([
{
meter: NOTES_CREATED_METER,
quantity: 1,
customer: project.orgId,
idempotencyKey: `${NOTES_CREATED_METER}.${note.noteId}`,
},
])
// The record is published: a second pass of the relay delivers nothing more.
expect(await app.deliver()).toBe(0)
})
it("gives a redelivered record the same audit event id and usage key, so the platform sees one", async () => {
const { app, project } = await fresh()
await create(project)
const record = (await app.data.outbox.pending(10))[0]
expect(record).toBeDefined()
const subscriber = app.registry.events["notes.NoteCreated"]?.subscribers[0]
const container = new Container()
const relayScope = { level: "org" as const, actor: systemActor("app.relay"), orgId: record?.orgId ?? "" }
container.value(FILES_SCOPE, relayScope)
// What the worker binds for every delivery: the relay's own transaction, which the notes module's
// notification is written through (adapters/notifications.ts).
container.value(NOTIFICATIONS_REPOS, app.data.forScope(relayScope))
container.value(FILES_AUDIT, app.audit)
container.value(FILES_USAGE, app.usage)
const deliverOnce = () =>
subscriber?.handler(
{ name: "notes.NoteCreated", payload: record?.payload },
{ container, correlationId: record?.id ?? "" },
)
await deliverOnce()
await deliverOnce()
expect(app.audit.events).toHaveLength(2)
expect(app.audit.events[0]?.eventId).toBeTruthy()
expect(app.audit.events[0]?.eventId).toBe(app.audit.events[1]?.eventId)
expect(app.usage.facts[0]?.idempotencyKey).toBe(app.usage.facts[1]?.idempotencyKey)
})
it("audits an edit once per version, so two edits are two events", async () => {
const { app, project } = await fresh()
const note = await create(project)
await run(project, "notes.note.update", { noteId: note.noteId, title: "A", body: "", expectedVersion: 1 })
await run(project, "notes.note.update", { noteId: note.noteId, title: "B", body: "", expectedVersion: 2 })
await app.deliver()
const edits = app.audit.events.filter((event) => event.action === "notes.note_updated")
expect(edits).toHaveLength(2)
expect(new Set(edits.map((event) => event.eventId)).size).toBe(2)
})
})
describe("attachments", () => {
it("records one usage event and one audit event for an upload, and one more audit event for the link", async () => {
const { app, project } = await fresh()
const note = await create(project)
await app.deliver()
app.audit.events.length = 0
app.usage.facts.length = 0
const stored = await app.upload(project, "minutes.txt", "agreed on Friday")
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["files.file_uploaded"])
expect(app.usage.facts).toEqual([
expect.objectContaining({ meter: "files_bytes_stored", quantity: stored.size, customer: project.orgId }),
])
const attached = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
filename: string
}
expect(attached.filename).toBe("minutes.txt")
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toEqual(["files.file_uploaded", "notes.attachment_added"])
expect(app.usage.facts).toHaveLength(1)
const seen = await detail(project, note.noteId)
expect(seen.attachments).toEqual([
expect.objectContaining({ fileId: stored.fileId, filename: "minutes.txt", contentType: "text/plain" }),
])
expect(seen.note.attachmentCount).toBe(1)
})
it("links a file once: attaching it again answers with the first link and records nothing more", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
const first = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
const second = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
expect(second.attachmentId).toBe(first.attachmentId)
await app.deliver()
expect(app.audit.events.filter((event) => event.action === "notes.attachment_added")).toHaveLength(1)
})
it("refuses a file that is not this project's, and one that does not exist, alike", async () => {
const { app, project } = await fresh()
const note = await create(project)
const other = await app.seedProject("globex", "owner@globex.test")
const foreign = await app.upload(other)
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: foreign.fileId }))).status,
).toBe(404)
expect(
(
await refusal(
run(project, "notes.attachment.add", { noteId: note.noteId, fileId: "00000000-0000-0000-0000-000000000000" }),
)
).status,
).toBe(404)
})
it(`allows at most ${MAX_ATTACHMENTS} attachments to one note`, async () => {
const { app, project } = await fresh()
const note = await create(project)
for (let at = 0; at < MAX_ATTACHMENTS; at += 1) {
const stored = await app.upload(project, `f${at}.txt`, `body ${at}`)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
}
const extra = await app.upload(project, "extra.txt", "one too many")
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: extra.fileId }))).status,
).toBe(409)
})
it("refuses a file type a note does not carry, though the files module stores it", async () => {
const { project } = await fresh()
const note = await create(project)
const sheet = await uploadViaProcedures(project.call, project.slugs, {
filename: "plan.csv",
contentType: "text/csv",
bytes: bytesOf("a,b"),
})
const refused = await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: sheet.fileId }))
expect(refused.status).toBe(422)
expect((await detail(project, note.noteId)).attachments).toEqual([])
})
it("removes one link, audits it, and leaves the file in the project", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
const link = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
await run(project, "notes.attachment.remove", { noteId: note.noteId, attachmentId: link.attachmentId })
expect((await detail(project, note.noteId)).attachments).toEqual([])
expect(((await run(project, "files.file.list")) as { files: unknown[] }).files).toHaveLength(1)
await app.deliver()
expect(app.audit.events.map((event) => event.action)).toContain("notes.attachment_removed")
})
it("moves the note's version with each attachment, so its live readers see the change", async () => {
// The live stream announces a change of `notes_note`; the attachment count and the attachment list
// are read through the note, so linking and unlinking a file each change the note's row.
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
const link = (await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
const linked = (await detail(project, note.noteId)).note
expect(linked).toMatchObject({ attachmentCount: 1, version: note.version + 1 })
await run(project, "notes.attachment.remove", { noteId: note.noteId, attachmentId: link.attachmentId })
expect((await detail(project, note.noteId)).note).toMatchObject({
attachmentCount: 0,
version: linked.version + 1,
})
})
it("does not remove another note's attachment through this note", async () => {
const { app, project } = await fresh()
const first = await create(project, "First")
const second = await create(project, "Second")
const stored = await app.upload(project)
const link = (await run(project, "notes.attachment.add", { noteId: first.noteId, fileId: stored.fileId })) as {
attachmentId: string
}
expect(
(
await refusal(
run(project, "notes.attachment.remove", { noteId: second.noteId, attachmentId: link.attachmentId }),
)
).status,
).toBe(404)
expect((await detail(project, first.noteId)).attachments).toHaveLength(1)
})
it("deleting a note removes its links, audits how many, and keeps the files", async () => {
const { app, project } = await fresh()
const note = await create(project)
for (const name of ["a.txt", "b.txt"]) {
const stored = await app.upload(project, name, name)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
}
await run(project, "notes.note.delete", { noteId: note.noteId })
await app.deliver()
const deleted = app.audit.events.find((event) => event.action === "notes.note_deleted")
expect(deleted?.data).toMatchObject({ attachmentCount: 2 })
expect(((await run(project, "files.file.list")) as { files: unknown[] }).files).toHaveLength(2)
})
it("drops a deleted file's links when the files module has deleted the object", async () => {
const { app, project } = await fresh()
const note = await create(project)
const stored = await app.upload(project)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
await run(project, "files.file.delete", { fileId: stored.fileId })
// The marking transaction, then the job that deletes the object and publishes `files.FileDeleted`.
await app.deliver()
await app.runJobs()
await app.deliver()
const seen = await detail(project, note.noteId)
expect(seen.attachments).toEqual([])
expect(seen.note.attachmentCount).toBe(0)
// The unlink is a change of the note, so its live readers see the count drop.
expect(seen.note.version).toBe(note.version + 2)
})
it("is switched off by the notes.attachments flag: the link is refused and the page is told", async () => {
const off: NotesFlags = { enabled: () => false }
const { app, project } = await fresh(off)
const note = await create(project)
const stored = await app.upload(project)
expect(
(await refusal(run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId }))).status,
).toBe(403)
expect((await detail(project, note.noteId)).attachmentsEnabled).toBe(false)
})
it("is on by default, with no flag service bound", async () => {
const { project } = await fresh()
const note = await create(project)
expect((await detail(project, note.noteId)).attachmentsEnabled).toBe(true)
})
})
describe("tenants", () => {
it("never shows one organization's notes or attachments to another", async () => {
const { app, project } = await fresh()
const other = await app.seedProject("globex", "owner@globex.test")
const note = await create(project)
const stored = await app.upload(project)
await run(project, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId })
expect(((await run(other, "notes.note.list")) as { notes: unknown[] }).notes).toEqual([])
expect((await refusal(run(other, "notes.note.get", { noteId: note.noteId }))).status).toBe(404)
// The other tenant's own slugs with this tenant's ids: the same 404 as an id nobody has.
expect(
(await refusal(run(other, "notes.attachment.add", { noteId: note.noteId, fileId: stored.fileId }))).status,
).toBe(404)
// This tenant's caller naming the other organization's slugs is refused outright.
const crossed = await refusal(project.call.callByName("notes.note.list", { ...other.slugs }))
expect([403, 404]).toContain(crossed.status)
})
})
+176
View File
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A module event delivered through the transactional outbox, end to end: `notes.NoteCreated` is written by
// the transaction that writes the note, the relay hands it to the notes module's subscriber in the relay's
// own transaction (`NOTIFICATIONS_REPOS`), and the subscriber tells the project's other members through the
// notifications kit. What is proven here: one inbox row per record and recipient, also when a delivery is
// repeated (the record-derived key), never for the author, and none when the note's transaction rolls back.
// The relay's own exactly-once claim (Postgres-family profiles) is proven in kit-jobs-pgboss, not here.
import { type NoteView, notesModule } from "@app/modules/notes"
import type { EventContext, TenantScope } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import type { DataSource } from "@conusai-kit/data"
import { FILES_SCOPE } from "@conusai-kit/files"
import { NOTIFICATIONS_REPOS } from "@conusai-kit/notifications"
import { describe, expect, it } from "vitest"
import { notesDeps } from "../modules/notes/adapters/procedures.js"
import { createNote } from "../modules/notes/core/notes.js"
import { makeNotesApp, type NotesApp, type SeededProject } from "./support/notes-app.js"
interface Inbox {
items: Array<{ code: string; category: string; key: string; title: string | null; href: string | null }>
}
/** A restricted project: its creator is its first member, and the second person is added as an editor. */
async function restrictedProject(app: NotesApp) {
const seeded = await app.seedProject("acme", "owner@acme.test")
await seeded.call.callByName("tenancy.project.create", {
org: seeded.slugs.org,
workspace: seeded.slugs.workspace,
slug: "launch",
name: "Launch",
moduleKey: "notes",
visibility: "restricted",
})
const slugs = { ...seeded.slugs, project: "launch" }
const member = await app.addMember(seeded, "member@acme.test", "member")
await seeded.call.callByName("tenancy.project.set-member", {
...slugs,
userId: member.actor.userId,
roleKey: "editor",
})
return { seeded, slugs, member }
}
/** The notes module's entries of a person's inbox; the kit's own (an organization ready, an invitation accepted) are not. */
const inboxOf = async (call: SeededProject["call"], org: string) => {
const inbox = (await call.callByName("notifications.inbox.list", { org })) as Inbox
return { items: inbox.items.filter((item) => item.key.startsWith("notes:")) }
}
describe("notes.NoteCreated, through the outbox, to the project's other members", () => {
it("gives the project's other member one inbox row with a link to the note, and the author none", async () => {
const app = makeNotesApp()
const { seeded, slugs, member } = await restrictedProject(app)
const note = (await member.call.callByName("notes.note.create", {
...slugs,
title: "Kick-off",
body: "",
})) as NoteView
// Nothing until the relay runs: the event is a record of the note's transaction, not a side effect of the request.
expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([])
await app.deliver()
expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([
expect.objectContaining({
category: "notice",
code: "custom",
key: `notes:${note.noteId}:created`,
title: "New note in Launch",
href: `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes/${note.noteId}`,
}),
])
expect((await inboxOf(member.call, slugs.org)).items).toEqual([])
// The record is published: a second pass delivers nothing, and the inbox still holds one row.
expect(await app.deliver()).toBe(0)
expect((await inboxOf(seeded.call, slugs.org)).items).toHaveLength(1)
})
it("is the same notification when the relay delivers a record again after a crash", async () => {
const app = makeNotesApp()
const { seeded, slugs, member } = await restrictedProject(app)
await member.call.callByName("notes.note.create", { ...slugs, title: "Kick-off", body: "" })
const record = (await app.data.outbox.pending(10)).find((entry) => entry.name === "notes.NoteCreated")
if (!record) throw new Error("expected the note's outbox record")
const scope: TenantScope = { level: "org", actor: systemActor("app.relay"), orgId: record.orgId }
const subscribers = app.registry.events["notes.NoteCreated"]?.subscribers ?? []
const deliverOnce = async () => {
for (const subscriber of subscribers) {
const container = app.root.child()
container.value(FILES_SCOPE, scope)
container.value(NOTIFICATIONS_REPOS, app.data.forScope(scope))
const ctx: EventContext = { container, correlationId: record.id }
await subscriber.handler({ name: record.name, payload: record.payload }, ctx)
}
}
await deliverOnce()
await deliverOnce()
expect((await inboxOf(seeded.call, slugs.org)).items).toHaveLength(1)
})
it("tells the other member, not the author, when the project's creator writes the note", async () => {
const app = makeNotesApp()
const { seeded, slugs, member } = await restrictedProject(app)
await seeded.call.callByName("notes.note.create", { ...slugs, title: "Plan", body: "" })
await app.deliver()
expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([])
expect((await inboxOf(member.call, slugs.org)).items).toHaveLength(1)
})
it("writes the title in the recipient's own language", async () => {
const app = makeNotesApp()
const { seeded, slugs, member } = await restrictedProject(app)
await seeded.call.callByName("tenancy.me.update", { locale: "lt" })
await member.call.callByName("notes.note.create", { ...slugs, title: "Planas", body: "" })
await app.deliver()
expect((await inboxOf(seeded.call, slugs.org)).items.map((item) => item.title)).toEqual([
"Naujas užrašas projekte „Launch“",
])
})
it("tells nobody in a project without named members, where the whole workspace would be noise", async () => {
const app = makeNotesApp()
const seeded = await app.seedProject("acme", "owner@acme.test")
const member = await app.addMember(seeded, "member@acme.test", "member")
await member.call.callByName("notes.note.create", { ...seeded.slugs, title: "Open", body: "" })
await app.deliver()
expect((await inboxOf(seeded.call, seeded.slugs.org)).items).toEqual([])
})
it("publishes nothing when the note's transaction rolls back, so nobody is told about a note that is not there", async () => {
const app = makeNotesApp()
const { seeded, slugs, member } = await restrictedProject(app)
await member.call.callByName("notes.note.create", { ...slugs, title: "Kept", body: "" })
const [kept] = await app.data.outbox.pending(500).then((all) => all.filter((r) => r.name === "notes.NoteCreated"))
if (!kept) throw new Error("expected the note's outbox record")
await app.deliver()
const { noteId, orgId, workspaceId, projectId } = kept.payload as {
noteId: string
orgId: string
workspaceId: string
projectId: string
}
const scope = { level: "project" as const, actor: member.actor, orgId, workspaceId, projectId }
// The commit fails after the use case wrote the row and the record: both go, or neither does.
const failing: DataSource = {
...app.data,
unitOfWork: {
run: (s, fn) =>
app.data.unitOfWork.run(s, async (tx) => {
await fn(tx)
throw new Error("the commit failed")
}),
},
}
await expect(
createNote({ ...notesDeps(app.root), data: failing }, scope, { title: "Ghost", body: "" }),
).rejects.toThrow("the commit failed")
expect(await app.data.outbox.pending(500)).toEqual([])
expect(await app.deliver()).toBe(0)
expect((await inboxOf(seeded.call, slugs.org)).items.map((item) => item.key)).toEqual([`notes:${noteId}:created`])
const titles = ((await member.call.callByName("notes.note.list", slugs)) as { notes: NoteView[] }).notes
expect(titles.map((n) => n.title)).toEqual(["Kept"])
})
it("has one subscription per module and event, because the relay claims a delivery per record and module", () => {
// A second subscription of one module to one event would be claimed by the first and never run.
const app = makeNotesApp()
for (const [name, entry] of Object.entries(app.registry.events)) {
const modules = entry.subscribers.map((subscriber) => subscriber.module)
expect(modules, name).toEqual([...new Set(modules)])
}
expect(notesModule.requires).toContain("notifications")
})
})
+292
View File
@@ -0,0 +1,292 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Observability of the server processes (spec §9.5, plan item AT4.2): errors, traces and logs
// configured from the deployment contract, carrying the request's trace id, with secrets and tokens
// taken out before anything leaves the process, and a plain no-op with a clear log line when the
// deployment has no DSN. The SDK is the real @conusai-sdk/observe-node; only the network is a
// recording `fetch`.
import {
createRedactor,
FILTERED,
loadAppEnv,
observabilityConfig,
observabilityLine,
secretsOf,
startObservability,
} from "@app/config"
import { ids, withRequest } from "@conusai-sdk/observe-node"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
const TRACEPARENT = `00-${TRACE_ID}-00f067aa0ba902b7-01`
const SECRET = "s3cr3t-session-key-0123456789abcdef"
const TOKEN = "dt_0123456789abcdef0123456789abcdef"
const base: Record<string, string> = {
NODE_ENV: "test",
CONUSAI_FAKE_PLATFORM: "1",
CONUSAI_API_URL: "http://127.0.0.1:4010/api",
CONUSAI_API_TOKEN: TOKEN,
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "9",
POSTGRES_URL: "postgresql://app_app:hunter2hunter2@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:another-long-pass@db:5432/app",
AUTH_SESSION_SECRET: SECRET,
}
const appEnv = (extra: Record<string, string> = {}, service: "web" | "worker" = "web") =>
loadAppEnv(service, { ...base, ...extra })
describe("the observability settings", () => {
it("reads release, environment and service name from the deployment contract", () => {
const env = appEnv({
CONUSAI_DSN: "https://pub@errors.example.test/42",
CONUSAI_RELEASE: "2026.10.1",
CONUSAI_ENVIRONMENT: "preview",
OTEL_SERVICE_NAME: "acme-review",
OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test",
})
expect(observabilityConfig("web", env.deployment, env.production)).toMatchObject({
errors: true,
otlp: true,
release: "2026.10.1",
environment: "preview",
serviceName: "acme-review",
})
})
it("falls back to OTEL_SERVICE_VERSION, then to `dev`, and names the run mode when the platform names no environment", () => {
const platformRelease = appEnv({ OTEL_SERVICE_VERSION: "abc1234" })
expect(observabilityConfig("worker", platformRelease.deployment, false)).toMatchObject({
release: "abc1234",
environment: "development",
serviceName: "app-worker",
})
expect(observabilityConfig("web", appEnv().deployment, true)).toMatchObject({
release: "dev",
environment: "production",
})
})
it("says in one line what a process without a DSN will not send", () => {
const env = appEnv()
const line = observabilityLine(observabilityConfig("web", env.deployment, false))
expect(line).toContain("errors off (CONUSAI_DSN is not set)")
expect(line).toContain("logs and traces off (OTEL_EXPORTER_OTLP_ENDPOINT is not set)")
})
})
describe("redaction", () => {
const redact = createRedactor(secretsOf({ ...base }))
it.each([
["MONGODB_URL", "mongodb://app:encoded%2Fpassword-long@db:27017/app", "encoded/password-long"],
["CONVEX_ADMIN_KEY", "opaque-convex-administrator-value", "opaque-convex-administrator-value"],
["CONVEX_SELF_HOSTED_ADMIN_KEY", "opaque-self-hosted-convex-admin-value", "opaque-self-hosted-convex-admin-value"],
])("removes %s credentials from profile driver errors", (name, value, credential) => {
const profileRedactor = createRedactor(secretsOf({ [name]: value }))
expect(profileRedactor.text(`driver rejected ${credential}`)).toBe(`driver rejected ${FILTERED}`)
expect(profileRedactor.deep({ exception: { value: `driver rejected ${credential}` } })).toEqual({
exception: { value: `driver rejected ${FILTERED}` },
})
})
it("takes the process's own secret values out of any string, wherever they appear", () => {
expect(redact.text(`session ${SECRET} and token ${TOKEN}`)).toBe(`session ${FILTERED} and token ${FILTERED}`)
// A connection string's password goes on its own, too, as the driver quotes it in an error.
expect(redact.text("password authentication failed: hunter2hunter2")).toBe(
`password authentication failed: ${FILTERED}`,
)
})
it.each([
["a bearer credential", "sent Authorization: Bearer abcdef0123456789xyz", "abcdef0123456789xyz"],
["a credential in a URL", "connect postgresql://user:p4ssw0rd-long@db:5432/app", "p4ssw0rd-long"],
["a JSON web token", "id eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJl", "eyJzdWIi"],
["an organization API key", "key cak_ab12cd34_0123456789abcdefghij", "0123456789abcdefghij"],
["an invitation link", "GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA", "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA"],
[
"an invitation link inside a return_to query",
"GET /auth/sign-in?return_to=%2Finvitations%2Facme%2FZm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
"Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
],
["an authorization code in a query", "GET /auth/callback?code=abc123&state=xyz789", "abc123"],
["an e-mail address", "invited ada@example.test", "ada@example.test"],
])("removes %s", (_name, text, secret) => {
const cleaned = redact.text(text)
expect(cleaned).not.toContain(secret)
expect(cleaned.length).toBeGreaterThan(0)
})
it("keeps what is not a secret: the organization in an invitation link, the path, the words", () => {
expect(redact.text("GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA failed")).toContain("/invitations/acme/")
expect(redact.text("procedure notes.note.create failed")).toBe("procedure notes.note.create failed")
})
it("filters a value under a key that names a secret, at any depth, and strings inside arrays", () => {
const cleaned = redact.deep({
headers: { authorization: "Bearer x", "x-csrf-token": "t", accept: "text/html" },
nested: { api_key: "k", session: { id: "s" }, list: [{ password: "p" }, "mail bob@example.test"] },
count: 3,
})
expect(cleaned).toEqual({
headers: { authorization: FILTERED, "x-csrf-token": FILTERED, accept: "text/html" },
nested: { api_key: FILTERED, session: FILTERED, list: [{ password: FILTERED }, "mail [email]"] },
count: 3,
})
})
it("survives a value that points at itself", () => {
const loop: Record<string, unknown> = { name: "loop" }
loop.self = loop
expect(redact.deep(loop)).toEqual({ name: "loop", self: FILTERED })
})
it("scrubs an error event but leaves the ids that join it to its request", () => {
const event = redact.event({
message: `failed for ${SECRET}`,
request: { url: "https://app.example.test/auth/callback?code=abc&state=def", headers: { cookie: "s=1" } },
contexts: {
trace: { trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" },
conusai: { session_id: "8f6a1c0e-2b8d-4a43-9d6e-5d1f6c2b7a90", request_id: "req-1" },
runtime: { token: "x" },
},
exception: { values: [{ type: "Error", value: "bad Bearer abcdef0123456789xyz" }] },
})
expect(JSON.stringify(event)).not.toContain(SECRET)
expect(event.request?.url).toBe(`https://app.example.test/auth/callback?code=${FILTERED}&state=${FILTERED}`)
expect(event.request?.headers?.cookie).toBe(FILTERED)
expect(event.exception?.values?.[0]?.value).toBe(`bad Bearer ${FILTERED}`)
expect(event.contexts?.trace).toEqual({ trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" })
expect(event.contexts?.conusai).toMatchObject({ request_id: "req-1" })
expect(event.contexts?.runtime).toEqual({ token: FILTERED })
})
})
describe("startObservability", () => {
const handlers = new Map<string, unknown[]>()
beforeEach(() => {
// Record the process handlers the SDK installs instead of installing them in the test worker.
vi.spyOn(process, "on").mockImplementation(((event: string, fn: unknown) => {
handlers.set(event, [...(handlers.get(event) ?? []), fn])
return process
}) as never)
vi.spyOn(process, "off").mockImplementation((() => process) as never)
})
afterEach(() => {
handlers.clear()
vi.restoreAllMocks()
})
it("is a no-op with one clear log line when the deployment has no DSN", async () => {
const lines: string[] = []
const observe = startObservability(appEnv(), { env: base, out: (line) => lines.push(line) })
expect(observe.errors).toBeUndefined()
expect(observe.otlp).toBeUndefined()
observe.captureException(new Error("nobody listens"))
await observe.shutdown()
expect(lines).toHaveLength(1)
expect(JSON.parse(lines[0] as string)).toMatchObject({
level: "info",
service: "app-web",
message: expect.stringContaining("errors off (CONUSAI_DSN is not set)"),
})
expect(handlers.size).toBe(0)
})
it("sends an error with the request's trace and request ids, and no secret", async () => {
const sent: Array<{ url: string; body: string }> = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body
const text = typeof body === "string" ? body : Buffer.from(body as Uint8Array).toString("utf8")
sent.push({ url: String(url), body: text })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ CONUSAI_DSN: "https://pub123@errors.example.test/42", CONUSAI_RELEASE: "r-1" }),
{ env: base, out: () => undefined, fetch: recording },
)
expect(observe.errors).toBeDefined()
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-77" }), () => {
observe.captureException(new Error(`db down at postgresql://app_app:hunter2hunter2@db:5432/app with ${TOKEN}`), {
procedure: "notes.note.create",
})
})
await observe.errors?.flush(2_000)
const [request] = sent
expect(request?.url).toContain("https://errors.example.test/api/42/envelope/")
expect(request?.body).toContain(TRACE_ID)
expect(request?.body).toContain("req-77")
expect(request?.body).toContain('"release":"r-1"')
expect(request?.body).not.toContain("hunter2hunter2")
expect(request?.body).not.toContain(TOKEN)
expect(request?.body).toContain(FILTERED)
await observe.errors?.close()
})
it("sends a log line over OTLP with the request's trace id, redacted, and prints it as a JSON line", async () => {
const posts: Array<{ url: string; body: string }> = []
const lines: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
posts.push({ url: String(url), body: String(init?.body) })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel", OTEL_SERVICE_NAME: "acme-review" }),
{ env: base, out: (line) => lines.push(line), fetch: recording },
)
expect(observe.otlp?.enabled).toBe(true)
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-78" }), () => {
expect(ids().trace_id).toBe(TRACE_ID)
observe.log.warn(`token ${TOKEN} refused`, { "conusai.org_slug": "acme", password: "p" })
})
await observe.otlp?.flush(2_000)
const logs = posts.filter((post) => post.url.endsWith("/otel/v1/logs"))
expect(logs.length).toBeGreaterThan(0)
const body = logs.map((post) => post.body).join("\n")
expect(body).toContain(TRACE_ID)
expect(body).toContain("acme-review")
expect(body).not.toContain(TOKEN)
expect(body).not.toContain('"p"')
const printed = lines
.map((line) => JSON.parse(line) as Record<string, string>)
.find((line) => line.level === "warn")
expect(printed).toMatchObject({ trace_id: TRACE_ID, request_id: "req-78", "conusai.org_slug": "acme" })
expect(JSON.stringify(printed)).not.toContain(TOKEN)
expect(printed?.password).toBe(FILTERED)
await observe.shutdown()
})
it("runs a function as a span under the request's trace and returns what it returns", async () => {
const posts: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
if (String(url).endsWith("/otel/v1/traces")) posts.push(String(init?.body))
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel" }), {
env: base,
out: () => undefined,
fetch: recording,
})
const answer = withRequest(new Headers({ traceparent: TRACEPARENT }), () =>
observe.span("procedure notes.note.create", { "conusai.org_slug": "acme" }, () => 42),
)
expect(answer).toBe(42)
await observe.otlp?.flush(2_000)
const body = posts.join("\n")
expect(body).toContain("procedure notes.note.create")
expect(body).toContain(TRACE_ID)
expect(body).toContain("conusai.org_slug")
await observe.shutdown()
})
it("runs a function as itself when traces are off", () => {
const observe = startObservability(appEnv(), { env: base, out: () => undefined })
expect(observe.span("anything", {}, () => "plain")).toBe("plain")
})
})
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The browser half of observability (plan item AT4.2): `src/client.tsx` starts
// @conusai-sdk/observe only when the server wrote the settings into the document, with the options this
// app chose, and an error boundary reports through the same instance. The SDK is the real one with
// the recording transport of @conusai-sdk/observe-testing, so what is asserted is what would be sent.
import { createTestObserve } from "@conusai-sdk/observe-testing"
import { describe, expect, it, vi } from "vitest"
import {
browserObserveOptions,
observeMeta,
readBrowserSettings,
reportClientError,
startBrowserObserve,
} from "../apps/web/src/observe/browser.js"
/** A document with the meta element the server writes, as far as the module reads one. */
const documentWith = (attributes: Record<string, string> | undefined) => ({
querySelector: (selector: string) =>
attributes && selector === 'meta[name="app-observe"]'
? { getAttribute: (name: string) => attributes[name] ?? null }
: null,
})
describe("the settings the server writes into the document", () => {
it("round-trips through the meta element", () => {
const settings = { release: "2026.10.1", environment: "preview" }
expect(readBrowserSettings(documentWith(observeMeta(settings)) as never)).toEqual(settings)
})
it("reads nothing from a document without the element, or with it off", () => {
expect(readBrowserSettings(documentWith(undefined) as never)).toBeUndefined()
expect(readBrowserSettings(documentWith({ content: "off" }) as never)).toBeUndefined()
})
it("names neither ConusAI nor a build-time variable: the element is the app's own", () => {
expect(JSON.stringify(observeMeta({ release: "r", environment: "e" })).toLowerCase()).not.toContain("conusai")
})
})
describe("starting the browser SDK", () => {
it("starts nothing when the deployment has no observability", () => {
const start = vi.fn()
expect(startBrowserObserve(documentWith(undefined) as never, start)).toBeUndefined()
expect(start).not.toHaveBeenCalled()
})
it("starts opt-in analytics and traces, with no replay before its separate consent", () => {
const start = vi.fn((options) => createTestObserve({ ...options, modules: { errors: false } }))
startBrowserObserve(documentWith(observeMeta({ release: "r-9", environment: "production" })) as never, start)
expect(start).toHaveBeenCalledWith(
expect.objectContaining({ release: "r-9", environment: "production", consent: "opt-in", respectGPC: true }),
)
const options = start.mock.calls[0]?.[0]
expect(options?.modules).toEqual({ traces: true, analytics: true })
expect(options?.modules).not.toHaveProperty("replay")
expect(browserObserveOptions({ release: "r-9", environment: "production" })).toEqual(options)
})
it("sends an error a boundary caught, through the instance the page started", () => {
const instance = createTestObserve({ modules: { errors: false } })
startBrowserObserve(documentWith(observeMeta({ release: "r", environment: "e" })) as never, () => instance)
reportClientError(new Error("the page threw"))
expect(instance.mock.kinds()).toContain("error")
const [item] = instance.mock.items()
expect(JSON.stringify(item)).toContain("the page threw")
})
})
describe("a recorded edge grant without a current choice of this app", () => {
const withCookie = (cookie: string) => ({
...documentWith(observeMeta({ release: "r", environment: "e" })),
cookie,
})
const olderNotice = `app_consent=${encodeURIComponent(
JSON.stringify({ analytics: true, replay: false, at: new Date().toISOString(), notice: "2025-01" }),
)}`
it.each([
["no choice", "_conusai_consent=granted"],
["a malformed choice", "app_consent=%7Bnope; _conusai_consent=granted"],
["a choice under an older notice", `${olderNotice}; _conusai_consent=granted`],
])("is withdrawn on %s, so the edge stops setting its cookies", (_, cookie) => {
const instance = createTestObserve({ consent: "opt-in", modules: { errors: false } })
startBrowserObserve(withCookie(cookie) as never, () => instance, {})
expect(instance.consent.state()).toBe("denied")
})
it("writes nothing for a visitor the edge holds no grant for", () => {
const instance = createTestObserve({ consent: "opt-in", modules: { errors: false } })
startBrowserObserve(withCookie("") as never, () => instance, {})
expect(instance.consent.state()).toBe("pending")
})
})
+107
View File
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OpenAPI 3.1 document and its drift check (AT2.1 done_when).
//
// `openapi.json` in the repository root is this function's output. Regenerate it with
// `pnpm openapi` after changing a procedure's schemas; the diff is then part of the review, which
// is the point -- an input schema that changed is an API change somebody's client will feel.
import { appBrand } from "@app/config"
import { buildRegistry } from "@conusai-kit/core"
import { openapiDocument, openapiJson } from "@conusai-kit/host/http"
import { describe, expect, it } from "vitest"
import { appModules, moduleSelected } from "../modules/index.js"
const registry = buildRegistry([...appModules])
// The title is the app brand's name, as the mount serves it (router.ts): renaming the app in
// config/brand.ts is a customer's first edit and must not turn the drift check red (AT2.7).
const info = { title: `${appBrand.name} API`, version: registry.kit }
describe("the OpenAPI document", () => {
it("matches the committed openapi.json", async () => {
await expect(openapiJson(registry, info)).toMatchFileSnapshot("../openapi.json")
})
it("is OpenAPI 3.1 with one path per procedure", () => {
const document = openapiDocument(registry, info) as { openapi: string; paths: Record<string, unknown> }
expect(document.openapi).toBe("3.1.0")
expect(Object.keys(document.paths).length).toBe(registry.procedures.size)
expect(document.paths["/api/v1/tenancy.org.create"]).toBeTruthy()
})
it("describes every refusal as problem+json", () => {
const document = openapiDocument(registry, info) as {
paths: Record<string, { post: { responses: Record<string, { content?: Record<string, unknown> }> } }>
}
const operation = document.paths["/api/v1/tenancy.member.invite"]?.post
expect(Object.keys(operation?.responses ?? {})).toEqual(expect.arrayContaining(["200", "401", "404", "422", "429"]))
expect(operation?.responses["404"]?.content).toHaveProperty(["application/problem+json"])
})
it("requires an Idempotency-Key exactly where the contract declares one", () => {
type Operation = { parameters: { name: string; required?: boolean }[] }
const document = openapiDocument(registry, info) as {
paths: Record<string, { post?: Operation; put?: Operation }>
}
for (const [name, { def }] of registry.procedures) {
const path = document.paths[`/api/v1/${name}`]
const parameters = (path?.post ?? path?.put)?.parameters ?? []
const header = parameters.find((parameter) => parameter.name === "Idempotency-Key")
expect(Boolean(header), `${name} is idempotent=${def.idempotent}`).toBe(def.idempotent)
}
})
it("documents a procedure with a streamed body as a PUT: the bytes as the body, the rest of its input as the query", () => {
const document = openapiDocument(registry, info) as {
paths: Record<
string,
{
post?: unknown
put?: {
parameters: { name: string; in: string; required: boolean }[]
requestBody: { content: Record<string, { schema: unknown }> }
}
}
>
}
const streamed = [...registry.procedures].filter(([, { def }]) => def.stream !== undefined).map(([name]) => name)
const expected = ["files.file.putPart"]
if (moduleSelected("guides")) expected.push("guides.runtime.uploadFile")
expect(streamed.sort()).toEqual(expected)
const path = document.paths["/api/v1/files.file.putPart"]
expect(path?.post).toBeUndefined()
const query = path?.put?.parameters.filter((parameter) => parameter.in === "query")
expect(query?.map((parameter) => parameter.name).sort()).toEqual(["fileId", "org", "part", "project", "workspace"])
expect(query?.every((parameter) => parameter.required)).toBe(true)
// The body is the one field that is not a parameter, and it is binary.
expect(query?.map((parameter) => parameter.name)).not.toContain("body")
expect(path?.put?.requestBody.content["application/octet-stream"]?.schema).toEqual({
type: "string",
format: "binary",
})
})
it("documents file downloads as authenticated scoped GET streams rather than JSON or signed URLs", () => {
type DownloadOperation = {
parameters: { name: string; in: string; required: boolean }[]
responses: Record<string, { content?: Record<string, { schema?: unknown }> }>
}
const document = openapiDocument(registry, info) as {
paths: Record<string, { get?: DownloadOperation; post?: unknown }>
}
const path = document.paths["/api/v1/files.file.download"]
expect(path?.post).toBeUndefined()
expect(path?.get?.parameters.map(({ name, in: location, required }) => [name, location, required])).toEqual([
["org", "query", true],
["workspace", "query", true],
["project", "query", true],
["fileId", "query", true],
["offset", "query", false],
["length", "query", false],
["disposition", "query", false],
])
expect(path?.get?.responses["200"]?.content).toHaveProperty(
["application/octet-stream", "schema", "format"],
"binary",
)
})
})
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The pages a person sees when there is no page (spec §10.4): the error boundary, the loading state, and
// the shared shape of not found and forbidden. Rendered to markup, so what is asserted is what a screen
// reader is given: one heading, a sentence, an alert for the failure, and the way out. The browser half
// (a 404 and a 403 as statuses, axe on both) is e2e/shell.spec.ts.
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
import { BoundaryMessages, ErrorView } from "../apps/web/src/ui/boundary.js"
import { LoadingPage } from "../apps/web/src/ui/loading-page.js"
import { StateView } from "../apps/web/src/ui/state-view.js"
const english = {
title: "Something went wrong",
body: "The page could not be shown. Try again.",
code: "Error code: {digest}",
retry: "Try again",
home: "Back to the start",
}
const render = (error: Error & { digest?: string }, retry = vi.fn()): string =>
renderToStaticMarkup(
createElement(BoundaryMessages, { strings: english, children: createElement(ErrorView, { error, retry }) }),
)
describe("the error boundary", () => {
it("has one heading, the failure as an alert, a way to try again and a way out", () => {
const html = render(new Error("hidden detail"))
expect(html.match(/<h1/g)).toHaveLength(1)
expect(html).toContain("<h1>Something went wrong</h1>")
expect(html).toContain('role="alert">The page could not be shown. Try again.')
expect(html).toContain("<button")
expect(html).toContain("Try again</button>")
expect(html).toContain('href="/"')
expect(html).toContain("Back to the start</a>")
})
it("never prints the error's own message, which can carry a tenant's data, and prints the digest an operator searches the log for", () => {
const html = render(Object.assign(new Error("password=hunter2 for ada@example.test"), { digest: "3275165072" }))
expect(html).not.toContain("hunter2")
expect(html).not.toContain("ada@example.test")
expect(html).toContain("Error code: 3275165072")
})
it("prints no code when there is no digest", () => {
expect(render(new Error("x"))).not.toContain("Error code")
})
it("hands the error up to the boundary above when nothing gave it words", () => {
expect(() => renderToStaticMarkup(createElement(ErrorView, { error: new Error("up"), retry: vi.fn() }))).toThrow(
"up",
)
})
})
describe("the loading and the empty states", () => {
it("announce once, politely, and hide the spinner from a screen reader", () => {
const html = renderToStaticMarkup(createElement(LoadingPage, { label: "Loading" }))
expect(html).toContain('role="status"')
expect(html).toContain('aria-hidden="true"')
expect(html).toContain("Loading")
expect(html).not.toContain("<h1")
expect(html).not.toContain("<main")
})
it("give a state page a landmark, a heading, its words and its actions, in that order", () => {
const html = renderToStaticMarkup(
createElement(StateView, {
title: "Page not found",
actions: createElement("a", { href: "/" }, "Back"),
children: createElement("p", null, "Nothing here."),
}),
)
expect(html).toContain('<main data-slot="app-state">')
expect(html).toContain('class="kit-empty kit-page-state"')
expect(html).toContain("<h1>Page not found</h1>")
expect(html).toContain("Nothing here.")
expect(html).toContain('<a href="/">Back</a>')
})
})
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { createElement, Suspense } from "react"
import { renderToString } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
const loaders = vi.hoisted(() => ({ project: vi.fn(), workspace: vi.fn(), other: vi.fn(), broken: vi.fn() }))
vi.mock("@app/modules/pages", () => ({
modulePages: { notes: loaders.project, other: loaders.other, broken: loaders.broken },
workspacePages: { notes: loaders.workspace },
}))
const { createPageViews, prepareInitialModuleViews, projectPageViews } = await import("../apps/web/src/ui/page-views")
describe("initial module view hydration", () => {
it("lets the router mount after a failed warmup and preserves the error for its route", async () => {
const failure = new Error("module chunk unavailable")
loaders.broken.mockRejectedValue(failure)
await expect(
prepareInitialModuleViews([
{ routeId: "/$org/$workspace/$project/$module/$", status: "success", loaderData: { module: "broken" } },
]),
).resolves.toBeUndefined()
await expect(projectPageViews.prepare("broken")).rejects.toBe(failure)
expect(loaders.broken).toHaveBeenCalledTimes(1)
})
it("renders prepared server markup synchronously and loads no other module", async () => {
const selected = vi.fn(async () => ({ View: () => createElement("a", { href: "/ready" }, "Ready") }))
const other = vi.fn(async () => ({ View: () => createElement("div", {}, "Other") }))
const views = createPageViews({ selected, other })
await Promise.all([views.prepare("selected"), views.prepare("selected")])
const html = renderToString(
createElement(Suspense, { fallback: "loading fallback" }, createElement(views.viewOf("selected"))),
)
expect(html).toContain('<a href="/ready">Ready</a>')
expect(html).not.toContain("loading fallback")
expect(selected).toHaveBeenCalledTimes(1)
expect(other).not.toHaveBeenCalled()
expect(views.viewOf("selected")).toBe(views.viewOf("selected"))
expect(views.has("toString")).toBe(false)
expect(() => views.viewOf("unregistered")).toThrow("no page for module")
})
it("prepares only successful matched project and workspace page data", async () => {
loaders.project.mockResolvedValue({ View: () => createElement("div") })
loaders.workspace.mockResolvedValue({ View: () => createElement("div") })
await prepareInitialModuleViews([
{ routeId: "/$org/$workspace/$project/$module/$", status: "success", loaderData: { module: "notes" } },
{ routeId: "/$org/$workspace/modules/$module/$", status: "success", loaderData: { module: "notes" } },
{ routeId: "/$org", status: "success", loaderData: { module: "other" } },
{ routeId: "/$org/$workspace/$project/$module/$", status: "error", loaderData: { module: "other" } },
{ routeId: "/$org/$workspace/modules/$module/$", status: "notFound", loaderData: { module: "notes" } },
{ routeId: "/$org/$workspace/modules/$module/$", status: "redirected" },
{ routeId: "/$org/$workspace/modules/$module/$", status: "success", loaderData: { module: "unknown" } },
])
expect(loaders.project).toHaveBeenCalledTimes(1)
expect(loaders.workspace).toHaveBeenCalledTimes(1)
expect(loaders.other).not.toHaveBeenCalled()
})
})
+210
View File
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The organization's theme from the platform, read on the server and written as the page's stylesheet
// (brand-kit BK12). The platform's answers are fakes here: the derivation is the kit's own, and every
// test looks at what the app would write, not at how the kit fetches it.
import { readdirSync, readFileSync, statSync } from "node:fs"
import { createRequire } from "node:module"
import path from "node:path"
import { deriveTheme, resolvedThemeStyles } from "@conusai-kit/brand"
import { createPlatformThemeCache } from "@conusai-kit/brand/platform-theme"
import { afterEach, describe, expect, it, vi } from "vitest"
import {
cloudOrganizationLogo,
createPlatformThemeReader,
ORGANIZATION_LOGO_PATH,
organizationIdentity,
type PlatformThemeDeployment,
} from "../apps/web/src/server/platform-theme.js"
const root = path.resolve(import.meta.dirname, "..")
/** The platform's answer for the navy fixture: the kit's derivation as OKLCH role maps (BrandingResponse). */
function branding(etag: string) {
const styles = resolvedThemeStyles(deriveTheme({ primary: "#001f4d", accent: "#1f6feb" }))
const roles = (mode: "light" | "dark") =>
Object.fromEntries(Object.entries(styles[mode]).map(([name, value]) => [name.slice(2), value]))
return {
version: 1,
seed: { primary: "#001f4d", accent: "#1f6feb" },
light: roles("light"),
dark: roles("dark"),
logo_url: null,
etag,
}
}
type Reply = { status: number; body?: unknown; etag?: string }
/** A fetch that answers from a script and records every request. */
function platform(...replies: Reply[]) {
const calls: Array<{ url: string; headers: Record<string, string> }> = []
const fetch = (async (url: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(url), headers: { ...((init?.headers ?? {}) as Record<string, string>) } })
const reply = replies.shift()
if (!reply) throw new Error("no scripted reply left")
if (reply.status === 304) return new Response(null, { status: 304, headers: { etag: reply.etag ?? "" } })
return new Response(reply.body === undefined ? null : JSON.stringify(reply.body), {
status: reply.status,
headers: { "content-type": "application/json", ...(reply.etag ? { etag: reply.etag } : {}) },
})
}) as typeof globalThis.fetch
return { fetch, calls }
}
const deployment = (org: string | undefined): PlatformThemeDeployment => ({
CONUSAI_API_URL: "https://platform.example.test/api",
CONUSAI_API_TOKEN: "dt_test_fixture",
...(org === undefined ? {} : { CONUSAI_ORG_SLUG: org }),
})
describe("the organization's theme on the server", () => {
it("writes the organization's roles as the page's stylesheet, dark on the root and light under .light", async () => {
const { fetch, calls } = platform({ status: 200, body: branding('"etag-1"'), etag: '"etag-1"' })
const css = await createPlatformThemeReader({ fetch })(deployment("acme"))
expect(calls[0]?.url).toBe("https://platform.example.test/api/v1/orgs/acme/branding")
expect(calls[0]?.headers.authorization).toBe("Bearer dt_test_fixture")
expect(css.startsWith(":root{--background:oklch(")).toBe(true)
expect(css).toContain(".light{--background:oklch(")
expect(css).toContain("--primary:oklch(")
})
it("with no organization slug the app asks nothing and writes nothing: the kit defaults stand", async () => {
const { fetch, calls } = platform()
expect(await createPlatformThemeReader({ fetch })(deployment(undefined))).toBe("")
expect(await createPlatformThemeReader({ fetch })(deployment(" "))).toBe("")
expect(calls).toHaveLength(0)
})
it("a cleared cache reads the organization again, with no validator", async () => {
const first = platform({ status: 200, body: branding('"etag-1"'), etag: '"etag-1"' })
await createPlatformThemeReader({ fetch: first.fetch })(deployment("acme"))
const second = platform({ status: 200, body: branding('"etag-2"'), etag: '"etag-2"' })
await createPlatformThemeReader({ fetch: second.fetch })(deployment("acme"))
expect(second.calls[0]?.headers["if-none-match"]).toBeUndefined()
})
it("an unavailable platform (503) gives the kit defaults, an empty stylesheet", async () => {
const { fetch } = platform({ status: 503, body: { title: "Unavailable" } })
expect(await createPlatformThemeReader({ fetch })(deployment("acme"))).toBe("")
})
it("a 304 writes the cached theme again with the cached ETag as the validator", async () => {
const cache = createPlatformThemeCache()
const first = platform({ status: 200, body: branding('"etag-1"'), etag: '"etag-1"' })
const fresh = await createPlatformThemeReader({ fetch: first.fetch, cache })(deployment("acme"))
const again = platform({ status: 304, etag: '"etag-1"' })
const cached = await createPlatformThemeReader({ fetch: again.fetch, cache })(deployment("acme"))
expect(again.calls[0]?.headers["if-none-match"]).toBe('"etag-1"')
expect(cached).toBe(fresh)
})
it("a changed organization is never written with another organization's theme", async () => {
const cache = createPlatformThemeCache()
const acme = platform({ status: 200, body: branding('"etag-acme"'), etag: '"etag-acme"' })
const acmeCss = await createPlatformThemeReader({ fetch: acme.fetch, cache })(deployment("acme"))
const other = platform({ status: 404, body: { title: "Organization Not Found" } })
const otherCss = await createPlatformThemeReader({ fetch: other.fetch, cache })(deployment("other"))
expect(other.calls[0]?.url).toContain("/v1/orgs/other/branding")
expect(other.calls[0]?.headers["if-none-match"]).toBeUndefined()
expect(otherCss).toBe("")
expect(acmeCss).not.toBe("")
})
})
describe("the organization's name and logo on Cloud", () => {
const PNG = new Uint8Array(
readFileSync(createRequire(import.meta.url).resolve("@conusai-kit/brand/logo/favicon.png")),
)
const on = { CONUSAI_API_URL: "https://platform.example.test/api", CONUSAI_API_TOKEN: "dt", CONUSAI_ORG_SLUG: "logo" }
afterEach(() => vi.unstubAllGlobals())
it("names the app after the organization and serves its logo from the app, with the token only to the platform", async () => {
const seen: Array<{ url: string; authorization?: string }> = []
vi.stubGlobal("fetch", async (url: string | URL, init?: RequestInit) => {
const headers = (init?.headers ?? {}) as Record<string, string>
seen.push({ url: String(url), ...(headers.authorization ? { authorization: headers.authorization } : {}) })
if (String(url).endsWith("/branding")) {
return Response.json(
{ ...branding('"n"'), name: "Navy UAB", logo_url: "/blob/org/7/logo.png" },
{ headers: { etag: '"n"' } },
)
}
return new Response(PNG, { headers: { "content-type": "image/png" } })
})
expect(await organizationIdentity(on)).toEqual({ name: "Navy UAB", logo: ORGANIZATION_LOGO_PATH })
const logo = await cloudOrganizationLogo(on)
expect(logo?.type).toBe("image/png")
expect(seen.at(-1)).toEqual({
url: "https://platform.example.test/blob/org/7/logo.png",
authorization: "Bearer dt",
})
})
it("keeps the app's own when Cloud has none, and refuses what is not a logo", async () => {
vi.stubGlobal("fetch", async (url: string | URL) =>
String(url).endsWith("/branding")
? Response.json(
{ ...branding('"x"'), logo_url: "https://cdn.example.test/x.png" },
{ headers: { etag: '"x"' } },
)
: new Response("<html>", { headers: { "content-type": "image/png" } }),
)
const deployment = { ...on, CONUSAI_ORG_SLUG: "nologo" }
expect(await organizationIdentity(deployment)).toEqual({ logo: ORGANIZATION_LOGO_PATH })
expect(await cloudOrganizationLogo(deployment)).toBeNull()
expect(await organizationIdentity({ ...on, CONUSAI_ORG_SLUG: "" })).toEqual({})
})
})
describe("the app's code and its deployment contract", () => {
it("names no hex colour and no colour function in the app's pages, layout, stylesheet or server code", () => {
const files = listFiles(path.join(root, "apps/web/src"), /\.(?:tsx?|css)$/)
expect(files.some((file) => file.endsWith("src/routes/__root.tsx"))).toBe(true)
expect(files.some((file) => file.endsWith("src/ui/globals.css"))).toBe(true)
for (const file of files) {
const code = readFileSync(file, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "")
.replace(/(?<=\s)\/\/ .*$/gm, "")
const relative = path.relative(root, file)
expect(code, relative).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
expect(code, relative).not.toMatch(/\b(?:rgba?|hsla?|hwb|lab|lch|oklch)\(/)
}
})
it("reads only the font, type scale and radius brand tokens in its stylesheet, never a colour token (BK3)", () => {
const css = readFileSync(path.join(root, "apps/web/src/ui/globals.css"), "utf8")
const reads = css.match(/var\(--brand-[a-z0-9-]+/g) ?? []
expect(reads.length).toBeGreaterThan(0)
for (const read of reads) expect(read, read).toMatch(/--brand-(?:font|radius|text|leading|app-font-size)-/)
})
it("draws the product UI at the app's 16px root and its one type scale, not the site's 18px root", () => {
const css = readFileSync(path.join(root, "apps/web/src/ui/globals.css"), "utf8")
expect(css).toMatch(/html:root \{\s*font-size: var\(--brand-app-font-size-root\);/)
expect(css).not.toContain("--brand-font-size-root")
expect(css).not.toContain("--brand-font-display")
})
it("asks the platform for branding:read, and only that new scope", () => {
const contract = JSON.parse(readFileSync(path.join(root, "conusai.json"), "utf8")) as {
token: { permissions: string[] }
}
expect(contract.token.permissions).toContain("branding:read")
expect(contract.token.permissions).toEqual(expect.arrayContaining(["blob:read", "blob:write"]))
})
it("lists the files it adds as the template's own", () => {
const list = readFileSync(path.join(root, "TEMPLATE-FILES"), "utf8").split("\n")
expect(list).toContain("apps/web/src/server/platform-theme.ts")
expect(list).toContain("test/platform-theme.test.ts")
})
})
function listFiles(directory: string, pattern: RegExp): string[] {
return readdirSync(directory).flatMap((name) => {
const full = path.join(directory, name)
return statSync(full).isDirectory() ? listFiles(full, pattern) : pattern.test(name) ? [full] : []
})
}
+349
View File
@@ -0,0 +1,349 @@
// 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" // kit:documents
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" // kit: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, vi } from "vitest"
import { z } from "zod"
import { bindWebPorts } from "../apps/web/src/server/ports.js"
import { bindModulePorts } from "../apps/worker/src/ports.js"
// kit:documents:begin
const fixture = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as Record<string, unknown>
// kit:documents:end
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.value).toEqual({ answer: "four" })
const [call] = (await stateOf()).ai
expect(call?.request?.model).toBe(clients.model)
})
// kit:documents:begin
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)
})
// kit:documents:end
})
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, // kit:documents
DOCUMENT_MODEL, // kit:documents
DOCUMENT_OCR, // kit:documents
FILES_REGISTRY,
PROPOSALS_REGISTRY,
]),
).not.toContain(false)
// 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, // kit:documents
DOCUMENT_MODEL, // kit:documents
DOCUMENT_OCR, // kit:documents
FILES_REGISTRY,
PROPOSALS_REGISTRY,
FILES_AUDIT,
FILES_USAGE,
PROPOSALS_AUDIT,
]),
).not.toContain(false)
})
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")
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules(["documents"]),
}))
+99
View File
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The language and the theme a person chooses (spec §10.4): precedence, the cookies that carry a
// visitor's choice, and the roles and invitation states the shell words from the catalogues.
import { describe, expect, it } from "vitest"
import { cookieValue, localeOf, panelWidthOf, sidebarOpenOf, themeOf } from "../apps/web/src/server/preferences.js"
import { inviteErrorKey } from "../apps/web/src/ui/invite-accept.js"
import {
LOCALE_COOKIE,
PREFERENCE_SECONDS,
preferenceCookie,
THEME_COOKIE,
} from "../apps/web/src/ui/preference-cookies.js"
describe("the language", () => {
it("is the signed-in person's own setting before anything else", () => {
expect(
localeOf({ appLocale: "en", userLocale: "lt", cookieHeader: `${LOCALE_COOKIE}=en`, acceptLanguage: "en" }),
).toBe("lt")
})
it("is a visitor's cookie when nobody is signed in", () => {
expect(localeOf({ appLocale: "en", cookieHeader: `a=1; ${LOCALE_COOKIE}=lt; b=2` })).toBe("lt")
})
it("falls to the browser's language when it names one the app serves, then to the app's default", () => {
// A Lithuanian app and an English browser: English, and the other way round.
expect(localeOf({ appLocale: "lt", acceptLanguage: "en-GB,en;q=0.9" })).toBe("en")
expect(localeOf({ appLocale: "en", acceptLanguage: "lt-LT,en;q=0.8" })).toBe("lt")
// The browser's own order, by weight, among the languages the app serves.
expect(localeOf({ appLocale: "en", acceptLanguage: "en;q=0.4,lt;q=0.9" })).toBe("lt")
expect(localeOf({ appLocale: "lt", acceptLanguage: "fr-FR,de;q=0.9,en;q=0.1" })).toBe("en")
// A browser that names none of them, or none at all: the app's default.
expect(localeOf({ appLocale: "lt", acceptLanguage: "fr-FR,de;q=0.9" })).toBe("lt")
expect(localeOf({ appLocale: "lt" })).toBe("lt")
expect(localeOf({ appLocale: "xx", acceptLanguage: "lt-LT,en;q=0.8" })).toBe("lt")
expect(localeOf({ appLocale: "en", userLocale: "fr", cookieHeader: `${LOCALE_COOKIE}=de` })).toBe("en")
})
it("keeps a person's or a visitor's own choice before the browser's", () => {
expect(localeOf({ appLocale: "lt", userLocale: "lt", acceptLanguage: "en-GB" })).toBe("lt")
expect(localeOf({ appLocale: "lt", cookieHeader: `${LOCALE_COOKIE}=lt`, acceptLanguage: "en-GB" })).toBe("lt")
})
})
describe("the theme", () => {
it("is dark unless the cookie says light: the brand is dark-first", () => {
expect(themeOf(null)).toBe("dark")
expect(themeOf(`${THEME_COOKIE}=light`)).toBe("light")
expect(themeOf(`${THEME_COOKIE}=dark`)).toBe("dark")
expect(themeOf(`${THEME_COOKIE}=system`)).toBe("system")
expect(themeOf(`${THEME_COOKIE}=neon`)).toBe("dark")
})
})
describe("the cookies", () => {
it("are written for a year, to every path, and never to another site", () => {
expect(preferenceCookie(LOCALE_COOKIE, "lt")).toBe(
`app_locale=lt; Path=/; Max-Age=${PREFERENCE_SECONDS}; SameSite=Lax`,
)
expect(PREFERENCE_SECONDS).toBe(31_536_000)
})
it("read back what was written, and survive a value that is not valid percent-encoding", () => {
expect(cookieValue(`x=1; ${preferenceCookie(THEME_COOKIE, "light").split(";")[0]}`, THEME_COOKIE)).toBe("light")
expect(cookieValue("app_locale=%E0%A4%A", LOCALE_COOKIE)).toBeUndefined()
expect(cookieValue(undefined, LOCALE_COOKIE)).toBeUndefined()
})
})
describe("an invitation's states", () => {
it.each([
["not_found", "invalid"],
["invitation_used", "used"],
["invitation_revoked", "revoked"],
["invitation_expired", "expired"],
["already_member", "member"],
["conflict", "failed"],
["rate_limited", "failed"],
["", "failed"],
] as const)("words the code %j as %s", (code, key) => {
expect(inviteErrorKey(code)).toBe(key)
})
})
describe("the frame's layout", () => {
it("opens the sidebar unless the person closed it", () => {
expect(sidebarOpenOf(null)).toBe(true)
expect(sidebarOpenOf("app_sidebar=open")).toBe(true)
expect(sidebarOpenOf("app_sidebar=closed")).toBe(false)
})
it("keeps the panel's width only inside what the panel can take", () => {
expect(panelWidthOf(null)).toBeUndefined()
expect(panelWidthOf("app_panel_width=480")).toBe(480)
expect(panelWidthOf("app_panel_width=9000")).toBeUndefined()
expect(panelWidthOf("app_panel_width=wide")).toBeUndefined()
})
})
+98
View File
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Exercise the real server entry before the Start handler or any store is reached.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const state = vi.hoisted(() => ({
profile: "mariadb",
registryError: undefined as Error | undefined,
envError: undefined as Error | undefined,
handler: vi.fn(async () => new Response("supported handler")),
}))
vi.mock("@app/config", async (original) => {
const config = await original<typeof import("@app/config")>()
return {
...config,
get PROFILE() {
return state.profile
},
loadAppEnv: () => {
if (state.envError) throw state.envError
return {}
},
profileRegistry: (...args: Parameters<typeof config.profileRegistry>) => {
if (state.registryError) throw state.registryError
return config.profileRegistry(...args)
},
}
})
vi.mock("@conusai-kit/auth", async (original) => ({
...(await original<typeof import("@conusai-kit/auth")>()),
loadAuthConfig: () => ({}),
}))
vi.mock("@tanstack/react-start/server-entry", () => ({
default: { fetch: state.handler },
createServerEntry: (entry: unknown) => entry,
}))
vi.mock("../apps/web/src/server/guard", () => ({ PAGE_STATUS_HEADER: "x-conusai-page-status" }))
beforeEach(() => {
vi.resetModules()
state.profile = "mariadb"
state.registryError = undefined
state.envError = undefined
state.handler.mockClear()
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => vi.restoreAllMocks())
describe("the actual web entry's startup composition", () => {
// kit:profile-dialect:begin
it.each(["mariadb", "mongodb"])("classifies unsupported adopted %s modules before dispatch", async (profile) => {
state.profile = profile
const { default: entry } = await import("../apps/web/src/server")
const request = new Request("https://fixture.example.test/api/ready")
const response = await entry.fetch(request)
expect(response.status).toBe(503)
expect(response.headers.get("cache-control")).toBe("no-store")
expect(response.headers.get("retry-after")).toBe("30")
expect(await response.json()).toMatchObject({
ready: false,
checks: { contract: false },
reason: expect.stringContaining("agent, assistant"),
})
expect(state.handler).not.toHaveBeenCalled()
const health = await entry.fetch(new Request("https://fixture.example.test/api/health"))
expect(health.status).toBe(503)
expect(await health.json()).toEqual({ status: 503, code: "not_configured" })
})
// kit:profile-dialect:end
it("keeps the supported Postgres entry on the actual handler", async () => {
state.profile = "postgres"
const { default: entry } = await import("../apps/web/src/server")
const response = await entry.fetch(new Request("https://fixture.example.test/api/health"))
expect(response.status).toBe(200)
expect(await response.text()).toBe("supported handler")
expect(state.handler).toHaveBeenCalledTimes(1)
})
it("preserves existing environment refusal without exposing a quoted value", async () => {
state.envError = new Error('CONUSAI_API_URL is "https://synthetic-secret.invalid"')
const { default: entry } = await import("../apps/web/src/server")
const response = await entry.fetch(new Request("https://fixture.example.test/api/ready"))
expect(response.status).toBe(503)
expect(await response.text()).not.toContain("synthetic-secret")
expect(state.handler).not.toHaveBeenCalled()
})
// kit:profile-dialect:begin
it("does not classify an unexpected registry failure as a configuration refusal", async () => {
const unexpected = new Error("synthetic unexpected registry failure")
state.registryError = unexpected
const { default: entry } = await import("../apps/web/src/server")
await expect(entry.fetch(new Request("https://fixture.example.test/api/ready"))).rejects.toBe(unexpected)
expect(state.handler).not.toHaveBeenCalled()
})
// kit:profile-dialect:end
})
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `/api/ready` (spec §13): the migration level it compares against, read from this repository's
// folders. The verdict is `@conusai-kit/host/http`'s `checkReadiness` and its tests.
import { readdirSync } from "node:fs"
import { appRoot, expectedMigrations, GRANT_MIGRATIONS, missingMigrations } from "@app/config"
import { appModules } from "@app/modules"
import { buildRegistry } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
describe("the migration level", () => {
it("lists the app's files, then each module's, then the grants the migrate job adds", async () => {
const expected = await expectedMigrations(buildRegistry([...appModules]))
const files = readdirSync(`${appRoot()}/migrations/postgres`)
.filter((name) => name.endsWith(".sql"))
.map((name) => name.replace(/\.sql$/, ""))
.sort()
expect(expected.slice(0, files.length)).toEqual(files)
expect(expected).toContain("notes/0001_notes") // kit:notes
expect(expected.slice(-GRANT_MIGRATIONS.length)).toEqual([...GRANT_MIGRATIONS])
})
it("names what a database lacks", () => {
expect(missingMigrations(["a", "b", "c"], new Set(["a", "c", "z"]))).toEqual(["b"])
})
})
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The registry this app boots with (spec §4.1): built from the manifests in modules/index.ts and
// from nothing else, so what the router, the navigation and the permissions hold is what the
// modules declared.
import { buildRegistry } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
import { describe, expect, it } from "vitest"
import { appModules, domainModules, kernelModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
describe("the app registry", () => {
it("boots with the kernel modules a tenant app needs", () => {
const kernel = ["tenancy", "auth", "files", "proposals", "notifications"]
expect(kernelModules.map((module) => module.id)).toEqual(kernel)
expect(appModules.length).toBe(kernelModules.length + domainModules.length)
})
it("registers every tenancy and auth procedure", () => {
const names = [...registry.procedures.keys()]
expect(names).toContain("tenancy.org.create")
expect(names).toContain("tenancy.member.invite")
expect(names).toContain("auth.apiKey.create")
expect(registry.procedures.size).toBeGreaterThanOrEqual(15)
})
it("declares a permission for every tenant-scoped procedure", () => {
for (const [name, { def }] of registry.procedures) {
if (def.scope === "public" || def.scope === "user") continue
expect(registry.permissions[def.permission], `${name} declares ${def.permission}`).toBeTruthy()
}
})
it("lists every tenant-scoped procedure as needing an isolation fixture", () => {
const tenantScoped = [...registry.procedures.values()].filter(
({ def }) => def.scope !== "public" && def.scope !== "user",
)
expect(registry.isolationFixtures.length).toBe(tenantScoped.length)
})
it("has a table for every entity the app's schema must hold", () => {
const tables = [...entitiesOf(registry, "postgres"), ...KIT_JOBS_ENTITIES].map((entity) => entity.table)
expect(tables).toContain("organizations")
expect(tables).toContain("auth_sessions")
expect(tables).toContain("kit_processed_events")
expect(new Set(tables).size).toBe(tables.length)
})
it("gives the web shell a navigation entry per kernel area", () => {
expect(registry.nav.map((item) => item.href)).toEqual(expect.arrayContaining(["members", "settings", "api-keys"]))
})
})
+509
View File
@@ -0,0 +1,509 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Every subscriber accepts the delivery the worker's relay makes. The relay (`createOutboxRelay` of
// @conusai-kit/jobs-pgboss) runs each (record, subscriber) pair in the record's organization's scope with a system
// actor, and the worker binds each module's scope token to that delivery scope (`bindModulePorts`,
// apps/worker/src/ports.ts). A subscriber that works in a project builds the project from the record, comparing
// the record's organization with the delivery's.
//
// What runs here is the worker as it is deployed, on a disposable PostgreSQL with the three roles: the migrate job
// (`runAppMigrations`), the worker's runtime (`buildPostgresWorkerRuntime`: its data source, its pg-boss queue in
// the delivery's transaction), the worker's own port binding and pg-boss's worker (`createWorker`, its queues and
// its relay, driven a pass at a time), with the optional modules these events reach turned on the way an app
// turns them on. The platform is the fake one. A delivery that fails is
// a failure of this test, never a retry. Needs Docker.
import { execFileSync } from "node:child_process"
import { loadAppEnv } from "@app/config"
import { appModules } from "@app/modules"
import * as knowledge from "@app/modules/knowledge" // kit:knowledge
import { type Actor, createCaller, type ModuleManifest, newId, type TenantScope } from "@conusai-kit/core"
import {
beginDelete, // kit:knowledge
fileDeleted,
fileStored, // kit:knowledge
files, // kit:knowledge
filesDeps, // kit:knowledge
finishDeleteRecord, // kit:knowledge
platformBlobPathname, // kit:knowledge
} from "@conusai-kit/files"
import { runAppMigrations } from "@conusai-kit/host/migrate"
import { loadPostgresEnv, startObservability } from "@conusai-kit/host/postgres/config"
import { buildPostgresWorkerRuntime, type WorkerRuntime } from "@conusai-kit/host/worker"
import { createWorker, type Relay, type Worker, type WorkerLogEvent } from "@conusai-kit/jobs-pgboss"
import { createKnowledge } from "@conusai-kit/platform" // kit:knowledge
import { accountDataOf, createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy" // kit:knowledge
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
import { Pool } from "pg"
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import { bindModulePorts } from "../apps/worker/src/ports.js"
import { appSchedules } from "../apps/worker/src/schedules.js"
const APP_ROOT = new URL("..", import.meta.url).pathname
const docker = (...args: string[]): string =>
execFileSync("docker", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
/** The optional modules these deliveries reach, turned on the way an app turns them on. */
const TURNED_ON: readonly ModuleManifest[] = [
knowledge.knowledgeModule, // kit:knowledge
]
/** The app's modules with those turned on. */
function selectedModules(): readonly ModuleManifest[] {
const on = new Set(TURNED_ON.map((module) => module.id))
return [...appModules.filter((module) => !on.has(module.id)), ...TURNED_ON]
}
let container = ""
let owner: Pool
let platform: Awaited<ReturnType<typeof startFakePlatform>>
let runtime: WorkerRuntime
let worker: Worker
let relay: Relay
/** Each failed delivery, as the relay reports it: the module and why. */
const failures: string[] = []
let project: TenantScope & { level: "project"; workspaceId: string; projectId: string; actor: Actor }
/** The project owner's caller, for what a person does in a test. */
let call: ReturnType<typeof createCaller>
beforeAll(async () => {
const name = `app-relay-scope-${crypto.randomUUID()}`
// The three roles a linked managed database provides, made by the same script the dev stack and e2e run.
container = docker(
"run",
"--detach",
"--rm",
"--name",
name,
"--label",
"conusai.test=app-relay-scope",
"--publish",
"127.0.0.1::5432",
"--env",
"POSTGRES_PASSWORD=postgres",
"--env",
"POSTGRES_DB=appdev",
"--volume",
`${APP_ROOT}dev:/conusai-dev:ro`,
"--entrypoint",
"sh",
"postgres:18",
"/conusai-dev/postgres-entrypoint.sh",
"postgres",
)
let port = ""
for (const deadline = Date.now() + 10_000; !/^[1-9][0-9]*$/.test(port); ) {
port = docker("port", container, "5432/tcp").split(":").at(-1) ?? ""
if (Date.now() > deadline) throw new Error("the test database's port was not published")
if (!/^[1-9][0-9]*$/.test(port)) await new Promise((resolve) => setTimeout(resolve, 100))
}
const url = (role: string): string => `postgresql://appdev_${role}:appdev_${role}@127.0.0.1:${port}/appdev`
const ownerUrl = url("owner")
// The init script creates the roles after the server first starts, then it restarts: ready is the owner's login.
owner = new Pool({ connectionString: ownerUrl, max: 1, connectionTimeoutMillis: 1_000 })
for (const deadline = Date.now() + 90_000; ; ) {
try {
await owner.query("select 1")
break
} catch (error) {
if (Date.now() > deadline) throw error
await new Promise((resolve) => setTimeout(resolve, 250))
}
}
platform = await startFakePlatform()
const source = {
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",
// The deployment's workspace, project and environment, which address the platform's knowledge service.
CONUSAI_WORKSPACE_SLUG: "general",
CONUSAI_PROJECT_SLUG: "app",
CONUSAI_ENVIRONMENT: "test",
POSTGRES_URL: url("app"),
POSTGRES_READER_URL: url("reader"),
}
const modules = selectedModules()
// The migrate job, as compose.yaml runs it: it reads the two logins it grants to from the environment.
const saved = { POSTGRES_URL: process.env.POSTGRES_URL, POSTGRES_READER_URL: process.env.POSTGRES_READER_URL }
Object.assign(process.env, { POSTGRES_URL: source.POSTGRES_URL, POSTGRES_READER_URL: source.POSTGRES_READER_URL })
try {
await runAppMigrations({ ownerUrl, modules, root: APP_ROOT })
} finally {
for (const [key, value] of Object.entries(saved)) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
}
// The worker, as createPostgresWorker composes it for apps/worker/src/main.ts: its runtime, the app's port
// binding and pg-boss's worker, which creates the queues and serves them. Only the relay's timer is off: the
// test drives the worker's own relay a pass at a time.
loadAppEnv("worker", source)
const env = loadPostgresEnv("worker", source)
runtime = buildPostgresWorkerRuntime({ modules, env, observe: startObservability(env, { env: source }), source })
bindModulePorts(runtime.container, runtime.platform, runtime.registry)
// kit:knowledge:begin -- the knowledge port, bound as bindModulePorts binds it once an app has adopted knowledge
// (apps/worker/src/ports.ts `bindKnowledgePorts`). The shipped inclusion record has not adopted it; this test
// turns knowledge on by its manifest, so it binds the port the same way.
runtime.container.register(knowledge.KNOWLEDGE_SEARCH, (c) =>
knowledge.platformKnowledgeSearch(createKnowledge(runtime.platform.deployment), filesDeps(c), {
environment: runtime.platform.deployment.CONUSAI_ENVIRONMENT,
}),
)
// kit:knowledge:end
worker = createWorker({
registry: runtime.registry,
boss: runtime.boss,
claimGuard: runtime.claimGuard,
dataSource: runtime.data,
container: runtime.container,
schedules: appSchedules(),
relay: { enabled: false },
log: (event: WorkerLogEvent) => {
if (event.kind === "relay.failed") failures.push(`${event.event} -> ${event.subscriber}: ${String(event.error)}`)
},
})
await worker.start()
relay = worker.relay
// One organization with a project, made through the tenancy procedures as a person makes it.
const { data, registry } = runtime
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: "ada@example.test",
email: "ada@example.test",
displayName: "Ada",
locale: "en",
})
const actor: Actor = { userId: user.id }
const tenancy = createTenancyHooks({ data, registry })
const requests = runtime.container.child()
requests.value(TENANCY, tenancy)
call = createCaller({ registry, container: requests, tenancy }, { actor })
const { orgId } = (await call.callByName("tenancy.org.create", { slug: "acme", name: "Acme" })) as { orgId: string }
const { workspaceId } = (await call.callByName("tenancy.workspace.create", {
org: "acme",
slug: "ops",
name: "Ops",
visibility: "org",
})) as { workspaceId: string }
const { projectId } = (await call.callByName("tenancy.project.create", {
org: "acme",
workspace: "ops",
slug: "site",
name: "Site",
moduleKey: "notes",
})) as { projectId: string }
project = { level: "project", actor, orgId, workspaceId, projectId }
// What the setup published is delivered first, so each test drains only its own records.
await relay.drain()
expect(failures).toEqual([])
}, 300_000)
afterAll(async () => {
await worker?.stop()
await runtime?.close()
await owner?.end()
await platform?.close()
if (container) docker("rm", "--force", "--volumes", container)
})
beforeEach(() => {
failures.length = 0
})
/** The jobs pg-boss holds for one queue, with the tenant ids each was enqueued in. */
async function queued(job: string): Promise<Array<{ scope: unknown; payload: unknown }>> {
const { rows } = await owner.query<{ data: { scope: unknown; payload: unknown } }>(
"select data from pgboss.job where name = $1 order by created_on",
[job],
)
return rows.map((row) => ({ scope: row.data.scope, payload: row.data.payload }))
}
describe("the worker's relay delivers to every subscriber in the organization's scope", () => {
it("delivers a file deletion to every module that subscribes to it", async () => {
const fileId = newId()
// kit:knowledge:begin -- the deleted file was indexed into a collection
await runtime.data.forScope(project).of(knowledge.documents).insert({
collectionName: "contracts",
fileId,
filename: "contract.pdf",
status: "indexed",
error: null,
indexedAt: "2026-11-09T00:00:00.000Z",
requestedBy: project.actor.userId,
})
// kit:knowledge:end
await runtime.data.unitOfWork.run(project, (tx) =>
tx.publish(fileDeleted, {
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
fileId,
storageKey: `files/${fileId}`,
size: 0,
}),
)
const pass = await relay.drain()
expect(failures).toEqual([])
expect(pass.delivered).toBeGreaterThan(0)
// kit:knowledge:begin -- knowledge cancelled the document and asked for its removal, which the relay delivers
const [document] = (await runtime.data.forScope(project).of(knowledge.documents).list({})).items
expect(document?.status).toBe("cancelled")
await relay.drain()
expect(failures).toEqual([])
expect(await queued(knowledge.REMOVE_JOB)).toMatchObject([
{
scope: {
level: "project",
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
},
payload: { fileId, collectionName: "contracts" },
},
])
// kit:knowledge:end
})
// kit:knowledge:begin
it("delivers knowledge's own index request, which enqueues the job in the record's project", async () => {
const documentId = newId()
await runtime.data.unitOfWork.run(project, (tx) =>
tx.publish(knowledge.documentIndexRequested, {
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
documentId,
collectionName: "contracts",
fileId: newId(),
}),
)
await relay.drain()
expect(failures).toEqual([])
expect(await queued(knowledge.INDEX_JOB)).toMatchObject([
{
scope: {
level: "project",
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
},
payload: { documentId },
},
])
})
// kit:knowledge:end
// kit:knowledge:begin
/** A stored file of the project, indexed into a collection: what an upload and an index leave behind. */
async function indexedFile(): Promise<{ fileId: string; storageKey: string }> {
const storageKey = `files/${newId()}`
const stored = await runtime.data
.forScope(project)
.of(files)
.insert({
status: "stored",
storageKey,
filename: "contract.pdf",
contentType: "application/pdf",
size: 1024,
// A stored row always has its size, digest and time (kit-files `viewOf` refuses one without them).
sha256: "0".repeat(64),
uploadedBy: project.actor.userId,
storedAt: "2026-11-09T00:00:00.000Z",
uploadId: null,
declaredSize: 1024,
partSize: null,
deletingAt: null,
})
const fileId = stored.id
await runtime.data.forScope(project).of(knowledge.documents).insert({
collectionName: "contracts",
fileId,
filename: "contract.pdf",
status: "indexed",
error: null,
indexedAt: "2026-11-09T00:00:00.000Z",
requestedBy: project.actor.userId,
})
return { fileId, storageKey }
}
/** The remove job's rows for one file, as pg-boss holds them. */
async function removeJob(fileId: string) {
const { rows } = await owner.query<{ state: string; retry_count: number }>(
"select state, retry_count from pgboss.job where name = $1 and data->'payload'->>'fileId' = $2",
[knowledge.REMOVE_JOB, fileId],
)
return rows
}
/** The platform knowledge service's `remove` calls for one stored object. */
async function removalsOf(storageKey: string) {
const state = (await (await fetch(`${platform.url}/__e2e/state`)).json()) as {
knowledge: Array<{ route: string; documentRef?: string }>
}
return state.knowledge.filter((call) => call.route === "remove" && call.documentRef?.endsWith(storageKey))
}
it("removes a deleted file's indexed text from the knowledge service once, with the ref the file had (F407)", async () => {
const { fileId, storageKey } = await indexedFile()
// The files module's own deletion: the row is marked, then removed, and `FileDeleted` is published with it.
await beginDelete(filesDeps(runtime.container), project, fileId, project.actor.userId)
expect(await finishDeleteRecord(runtime.data, project, fileId)).toBe(true)
await relay.drain() // FileDeleted -> knowledge cancels the document and asks for its removal
await relay.drain() // documentRemoveRequested -> the remove job, which pg-boss's worker runs
expect(failures).toEqual([])
const removeJob = async () => {
const { rows } = await owner.query<{ state: string; retry_count: number }>(
"select state, retry_count from pgboss.job where name = $1 and data->'payload'->>'fileId' = $2",
[knowledge.REMOVE_JOB, fileId],
)
return rows
}
await expect.poll(async () => (await removeJob())[0]?.state, { timeout: 30_000 }).toBe("completed")
expect(await removeJob()).toEqual([{ state: "completed", retry_count: 0 }])
const state = (await (await fetch(`${platform.url}/__e2e/state`)).json()) as {
knowledge: Array<{ route: string; documentRef?: string }>
}
const removals = state.knowledge.filter((call) => call.route === "remove")
expect(removals.filter((call) => call.documentRef?.endsWith(storageKey))).toEqual([
expect.objectContaining({ documentRef: platformBlobPathname(project, storageKey) }),
])
expect(
(await runtime.data.forScope(project).of(knowledge.documents).list({})).items.filter(
(row) => row.fileId === fileId,
),
).toEqual([])
// A second run of the same job, as a redelivery makes it, finds nothing left to remove and does nothing.
const { rows: delivered } = await owner.query<{ payload: unknown }>(
"select data->'payload' as payload from pgboss.job where name = $1 and data->'payload'->>'fileId' = $2",
[knowledge.REMOVE_JOB, fileId],
)
const inProject = runtime.container.child()
inProject.value(knowledge.KNOWLEDGE_SCOPE, project)
await runtime.registry.jobs[knowledge.REMOVE_JOB]?.def.handler(
delivered[0]?.payload as never,
{
container: inProject,
} as never,
)
const again = (await (await fetch(`${platform.url}/__e2e/state`)).json()) as typeof state
expect(
again.knowledge.filter((call) => call.route === "remove" && call.documentRef?.endsWith(storageKey)),
).toHaveLength(1)
})
it("removes it once too when a person's removal is still waiting as the file is deleted (F407, review R1)", async () => {
const { fileId, storageKey } = await indexedFile()
// A member removes the document: it is cancelled and its removal is published...
await call.callByName("knowledge.document.remove", {
org: "acme",
workspace: "ops",
project: "site",
collectionName: "contracts",
fileId,
})
// ...and the file is deleted before the relay has delivered that removal, so its row is gone when the job runs.
await beginDelete(filesDeps(runtime.container), project, fileId, project.actor.userId)
expect(await finishDeleteRecord(runtime.data, project, fileId)).toBe(true)
await relay.drain()
await relay.drain()
expect(failures).toEqual([])
await expect.poll(async () => (await removeJob(fileId))[0]?.state, { timeout: 30_000 }).toBe("completed")
expect(await removeJob(fileId)).toEqual([{ state: "completed", retry_count: 0 }])
expect(await removalsOf(storageKey)).toEqual([
expect.objectContaining({ documentRef: platformBlobPathname(project, storageKey) }),
])
expect(
(await runtime.data.forScope(project).of(knowledge.documents).list({})).items.filter(
(row) => row.fileId === fileId,
),
).toEqual([])
})
it("delivers a stored PDF to knowledge's files processor, which enqueues the automatic intake in the upload's project", async () => {
const fileId = newId()
const sha256 = "a".repeat(64)
await runtime.data.unitOfWork.run(project, (tx) =>
tx.publish(fileStored, {
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
fileId,
storageKey: `files/${project.workspaceId}/${project.projectId}/${fileId}`,
contentType: "application/pdf",
size: 10,
sha256,
uploadedBy: project.actor.userId,
recovered: false,
}),
)
await relay.drain()
expect(failures).toEqual([])
expect(await queued(knowledge.AUTO_INDEX_JOB)).toMatchObject([
{
scope: {
level: "project",
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
},
payload: { fileId, contentType: "application/pdf", sha256 },
},
])
})
it("exports and erases intake attribution through the registered account share under the runtime role", async () => {
const share = accountDataOf(runtime.registry).find((entry) => entry.key === "knowledge")
if (!share?.export || !share.erase) throw new Error("Knowledge account share is missing")
const scopes: Array<typeof project> = []
for (const slug of ["account-own", "account-other"]) {
const { projectId } = (await call.callByName("tenancy.project.create", {
org: "acme",
workspace: "ops",
slug,
name: slug,
moduleKey: "notes",
})) as { projectId: string }
scopes.push({ ...project, projectId })
}
const own = scopes[0]
const other = scopes[1]
if (!own || !other) throw new Error("Missing account project fixtures")
const { autoIndex } = knowledge
for (const [scope, enabledBy] of [
[own, project.actor.userId],
[other, "another-person"],
] as const) {
await runtime.data.forScope(scope).of(autoIndex).insert({
collectionName: "contracts",
enabledBy,
enabledAt: "2026-11-11T00:00:00.000Z",
})
}
const context = { subject: project.actor.userId, container: runtime.container }
const exported = await share.export(context)
if (!exported) throw new Error("Knowledge account export is missing")
expect(exported.automaticIndexing).toEqual(
expect.arrayContaining([expect.objectContaining({ orgId: own.orgId, projectId: own.projectId })]),
)
expect(exported.automaticIndexing).not.toEqual(
expect.arrayContaining([expect.objectContaining({ projectId: other.projectId })]),
)
await share.erase(context)
await share.erase(context)
expect((await runtime.data.forScope(own).of(autoIndex).list()).items).toEqual([])
expect((await runtime.data.forScope(other).of(autoIndex).list()).items).toMatchObject([
{ enabledBy: "another-person" },
])
})
// kit:knowledge:end
})
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The first path segment is an organization's slug (`src/routes/$org*`), so every other top-level route
// the web shell mounts must be a slug no organization can take: kit-tenancy refuses the slugs in
// RESERVED_SLUGS when an organization is created. A new top-level route that is not reserved would
// let an organization of that name shadow it, or the route shadow the organization (R8: `/members`).
// `server.mjs` serves the build's static files before any route, so a folder of those counts as well.
// The same holds one level down: the third segment is a project's slug, so a static route beside
// `$org.$workspace.$project` (`modules`, F10's pages above a project) is a slug no project can take.
import { existsSync, readdirSync } from "node:fs"
import { RESERVED_PROJECT_SLUGS, RESERVED_SLUGS } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
const routesDir = new URL("../apps/web/src/routes/", import.meta.url)
const publicDir = new URL("../apps/web/public/", import.meta.url)
/** A route file's first URL segment: `_layout.` and `(group).` add none, so what follows them is the route. */
const firstSegment = (file: string): string | undefined =>
file
.replace(/\.tsx?$/, "")
.split(".")
.find((part) => !/^[_(]/.test(part))
/** The routes' top-level segments: not `$param`, `__root` or `index`. */
const startSegments = readdirSync(routesDir)
.map(firstSegment)
.filter((segment): segment is string => segment !== undefined && !segment.startsWith("$") && segment !== "index")
/** Folders `dist/client` holds: Vite's own (its default `assetsDir`) and whatever `public/` brings. */
const staticSegments = [
"assets",
...(existsSync(publicDir) ? readdirSync(publicDir, { withFileTypes: true }) : [])
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name),
]
const routeSegments = [...new Set([...startSegments, ...staticSegments])]
/** The static segments beside a project: `$org.$workspace.<segment>...`, but not the workspace's own `index`. */
const workspaceSegments = [
...new Set(
readdirSync(routesDir)
.map((file) => /^\$org\.\$workspace\.([^$.][^.]*)\./.exec(file)?.[1])
.filter((segment): segment is string => segment !== undefined && segment !== "index"),
),
]
describe("top-level routes", () => {
it("finds the routes the shell mounts, or this suite proves nothing", () => {
// Each is a route R8's rule applies to; the list is what apps/web/ROUTES.md names at the top level.
expect(startSegments).toEqual(
expect.arrayContaining(["api", "auth", "invitations", "members", "onboarding", "settings"]),
)
})
it("reads a route behind a pathless layout as the route it is", () => {
expect(firstSegment("_public.pricing.tsx")).toBe("pricing")
expect(firstSegment("(marketing).pricing.tsx")).toBe("pricing")
expect(firstSegment("__root.tsx")).toBeUndefined()
})
it.each(routeSegments)("reserves /%s from organization slugs", (segment) => {
expect(RESERVED_SLUGS).toContain(segment)
})
})
describe("routes beside a project", () => {
it("finds the route of a module's pages above a project, or this suite proves nothing", () => {
expect(workspaceSegments).toContain("modules")
})
it.each(workspaceSegments)("reserves /<org>/<workspace>/%s from project slugs", (segment) => {
expect(RESERVED_PROJECT_SLUGS).toContain(segment)
})
})
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What this app hands the mount (apps/web/src/server/router.ts): its readiness per profile, its title
// and its live stream. The mount itself is `@conusai-kit/host/http` and its tests.
import { appBrand, PROFILE } from "@app/config"
import { beforeEach, describe, expect, it, vi } from "vitest"
const app = vi.hoisted(() => ({
runtime: {} as Record<string, unknown>,
ready: { ready: false, checks: { store: false, reader: false, migrations: false, platform: false } },
}))
vi.mock("../apps/web/src/server/app", () => ({
convexCapabilityStatus: async () => ({ ready: true }),
postgresHost: () => ({ readiness: async () => app.ready }),
webRuntime: () => app.runtime,
}))
vi.mock("../apps/web/src/server/request", () => ({
requestContext: async () => ({ principal: null, caller: {}, correlationId: "correlation" }),
}))
vi.mock("../apps/web/src/server/live", () => ({ serveLive: async () => new Response("the live stream") }))
const handled = async (path: string, init?: RequestInit): Promise<Response> => {
const { handle } = await import("../apps/web/src/server/router")
return handle(new Request(`https://app.example.test${path}`, init))
}
beforeEach(() => {
app.runtime = {
env: { deployment: { CONUSAI_API_URL: "http://127.0.0.1:9/api" } },
observe: { log: { warn: () => undefined } },
registry: { kit: "0.1.0", procedures: new Map() },
}
})
describe("this app's mount", () => {
it("serves its OpenAPI document under the app brand's name", async () => {
const document = (await (await handled("/api/openapi.json")).json()) as { info: { title: string } }
expect(document.info.title).toBe(`${appBrand.name} API`)
})
it("hands the live stream to the app's own server", async () => {
const answer = await handled("/api/v1/orgs/acme/live", { method: "POST" })
expect(await answer.text()).toBe("the live stream")
})
it.runIf(PROFILE === "postgres")("answers /api/ready from the host, 503 while it is not ready", async () => {
const answer = await handled("/api/ready")
expect(answer.status).toBe(503)
expect(await answer.json()).toEqual(app.ready)
})
})
+199
View File
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The software bill of materials of the built image and the THIRD-PARTY-NOTICES generated from it
// (plan item AT5.1): the SBOM reads what is installed in a pnpm store (a runtime dependency and not a
// link to one, once per version), and the notices list every component of it, with each licence text
// printed once however many packages share it. The committed THIRD-PARTY-NOTICES is checked against
// the dependencies this template declares.
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { dpkgComponents, npmComponents, sbom } from "../scripts/sbom.mjs"
import { notices, unlisted } from "../scripts/third-party-notices.mjs"
const APP_ROOT = new URL("..", import.meta.url).pathname
const made: string[] = []
afterEach(() => {
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
})
const MIT = "MIT License\n\nCopyright (c) 2020 Someone\n\nPermission is hereby granted, free of charge, ..."
/** A pnpm virtual store: each entry holds its own package and links to the packages it depends on. */
function image(): string {
const root = mkdtempSync(join(tmpdir(), "sbom-"))
made.push(root)
const put = (path: string, text: string) => {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), text)
}
const store = "node_modules/.pnpm"
put("package.json", JSON.stringify({ name: "demo-app", version: "1.2.3" }))
put(
`${store}/alpha@1.0.0/node_modules/alpha/package.json`,
JSON.stringify({ name: "alpha", version: "1.0.0", license: "MIT" }),
)
put(`${store}/alpha@1.0.0/node_modules/alpha/LICENSE`, MIT)
put(
`${store}/beta@2.0.0/node_modules/beta/package.json`,
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
)
put(`${store}/beta@2.0.0/node_modules/beta/LICENSE.md`, MIT)
// the same version under two peer sets is one component
put(
`${store}/beta@2.0.0_react@19/node_modules/beta/package.json`,
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
)
put(
`${store}/@scope+gamma@3.0.0/node_modules/@scope/gamma/package.json`,
JSON.stringify({
name: "@scope/gamma",
version: "3.0.0",
license: { type: "ISC" },
repository: { url: "git+https://example.test/gamma.git" },
}),
)
put(
`${store}/@conusai-kit+core@0.1.0/node_modules/@conusai-kit/core/package.json`,
JSON.stringify({ name: "@conusai-kit/core", version: "0.1.0", license: "LicenseRef-ConusAI-Template-1.0" }),
)
put(
`${store}/delta@4.0.0/node_modules/delta/package.json`,
JSON.stringify({ name: "delta", version: "4.0.0", license: "(MIT OR Apache-2.0)" }),
)
// alpha depends on beta: a link inside alpha's entry, which is not a second copy of beta
symlinkSync(join(root, `${store}/beta@2.0.0/node_modules/beta`), join(root, `${store}/alpha@1.0.0/node_modules/beta`))
return root
}
describe("the SBOM of an image", () => {
it("holds each installed package once, with its licence and the text of its licence file", () => {
const components = npmComponents(image())
expect(components.map((component) => component["bom-ref"])).toEqual([
"pkg:npm/%40conusai-kit/core@0.1.0",
"pkg:npm/%40scope/gamma@3.0.0",
"pkg:npm/alpha@1.0.0",
"pkg:npm/beta@2.0.0",
"pkg:npm/delta@4.0.0",
])
const alpha = components.find((component) => component.name === "alpha")
expect(alpha?.licenses[0]?.license).toMatchObject({ id: "MIT", text: { content: MIT } })
expect(components.find((component) => component.name === "gamma")).toMatchObject({
group: "@scope",
licenses: [{ license: { id: "ISC" } }],
externalReferences: [{ type: "vcs", url: "https://example.test/gamma" }],
})
expect(components.find((component) => component.name === "delta")?.licenses).toEqual([
{ expression: "(MIT OR Apache-2.0)" },
])
})
it("marks the ConusAI packages, whose licence is LICENSE-TEMPLATE, and names their licence as a reference", () => {
const core = npmComponents(image()).find((component) => component.name === "core")
expect(core?.properties).toContainEqual({ name: "conusai:component", value: "true" })
expect(core?.licenses[0]?.license).toEqual({ name: "LicenseRef-ConusAI-Template-1.0" })
})
it("is a CycloneDX 1.5 document with the application, the Node.js runtime and the packages", () => {
const document = sbom({ root: image(), dpkg: "/nonexistent", nodeVersion: "v24.1.0" })
expect(document).toMatchObject({
bomFormat: "CycloneDX",
specVersion: "1.5",
metadata: { component: { type: "application", name: "app", version: "0.0.0" } },
})
expect(document.components[0]).toMatchObject({ type: "platform", name: "node", version: "24.1.0" })
expect(document.components).toHaveLength(6)
})
it("reads the operating system packages that are installed, and no others", () => {
const dir = mkdtempSync(join(tmpdir(), "dpkg-"))
made.push(dir)
const status = join(dir, "status")
writeFileSync(
status,
[
"Package: libc6\nStatus: install ok installed\nVersion: 2.41-1\nArchitecture: amd64",
"Package: gone\nStatus: deinstall ok config-files\nVersion: 1.0",
"Package: zlib1g\nStatus: install ok installed\nVersion: 1:1.3.dfsg-1",
"",
].join("\n\n"),
)
const components = dpkgComponents(status, dir)
expect(components.map((component) => `${component.name} ${component.version}`)).toEqual([
"libc6 2.41-1",
"zlib1g 1:1.3.dfsg-1",
])
})
})
describe("THIRD-PARTY-NOTICES generated from an SBOM", () => {
const document = () =>
sbom({ root: image(), dpkg: "/nonexistent", name: "demo-app", version: "1.2.3", nodeVersion: "v24.1.0" })
it("lists every component, and prints a licence text shared by several packages once", () => {
const text = notices(document())
expect(unlisted(document(), text)).toEqual([])
expect(text).toContain("@conusai-kit/core@0.1.0")
expect(text).toContain("node@24.1.0")
expect(text.split("Permission is hereby granted").length - 1).toBe(1)
expect(text).toContain("alpha@1.0.0")
expect(text).toContain("beta@2.0.0")
expect(text).toContain("these packages ship no licence file; they declare ISC")
})
it("is the same text every time it is generated from the same SBOM", () => {
expect(notices(document())).toBe(notices(document()))
})
it("is checked against the SBOM: a component it does not list is named", () => {
const text = notices(document())
const more = document()
more.components.push({
type: "library",
"bom-ref": "pkg:npm/epsilon@5.0.0",
name: "epsilon",
version: "5.0.0",
purl: "pkg:npm/epsilon@5.0.0",
licenses: [],
})
expect(unlisted(more, text)).toEqual(["epsilon@5.0.0"])
// a version that moved is drift by name, not a missing component
const moved = document()
moved.components = moved.components.map((component) =>
component.name === "alpha" ? { ...component, version: "1.0.1" } : component,
)
expect(unlisted(moved, text)).toEqual(["alpha@1.0.1"])
expect(unlisted(moved, text, { byName: true })).toEqual([])
})
})
describe("the committed THIRD-PARTY-NOTICES", () => {
const text = readFileSync(join(APP_ROOT, "THIRD-PARTY-NOTICES"), "utf8")
it("says what it is generated from, and that it is not edited by hand", () => {
expect(text).toContain("software bill of materials of the built image")
expect(text).toContain("do not edit it by hand")
})
it("lists every runtime dependency this template declares, by name", () => {
const declared = new Set<string>()
for (const path of [
"package.json",
"apps/web/package.json",
"apps/worker/package.json",
"config/package.json",
"modules/package.json",
]) {
const manifest = JSON.parse(readFileSync(join(APP_ROOT, path), "utf8")) as {
dependencies?: Record<string, string>
}
for (const name of Object.keys(manifest.dependencies ?? {})) if (!name.startsWith("@app/")) declared.add(name)
}
expect(declared.size).toBeGreaterThan(15)
const missing = [...declared].filter((name) => !text.includes(` ${name}@`))
expect(missing).toEqual([])
})
})
+101
View File
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The demo data of a development run (apps/worker/src/seed.ts): two organizations, one with a tenant
// brand, written with the kit's own use cases as the people who may write them, and written once --
// a second run changes nothing. The data source is the in-memory one, which the kit proves to behave
// as Postgres does (the adapter contract suite); the procedures, the permission checks and the
// invitation flow are the real ones.
import { DEV_LOGIN_ISSUER } from "@conusai-kit/auth"
import { users } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { DEMO_ORGANIZATIONS, DEMO_PEOPLE, seedDemo } from "../apps/worker/src/seed.js"
import { makeNotesApp } from "./support/notes-app.js"
const seeded = async () => {
const app = makeNotesApp()
const deps = { data: app.data, registry: app.registry, container: app.root }
return { app, deps, summary: await seedDemo(deps) }
}
describe("the demo data", () => {
it("makes two organizations, and the people in them with the roles the demo describes", async () => {
const { app, summary } = await seeded()
expect(summary.organizations.map((org) => org.slug)).toEqual(["acme-review", "northwind"])
const rolesOf = async (email: string) => {
const rows = await app.data.global(users).list({ where: [{ field: "email", op: "eq", value: email }], limit: 1 })
const me = (await app.caller({ userId: rows.items[0]?.id ?? "" }).callByName("tenancy.me.get", undefined)) as {
organizations: Array<{ slug: string; role: string }>
}
return Object.fromEntries(me.organizations.map((org) => [org.slug, org.role]))
}
expect(await rolesOf("ada@example.test")).toEqual({ "acme-review": "owner", northwind: "admin" })
expect(await rolesOf("grace@example.test")).toEqual({ "acme-review": "member", northwind: "owner" })
expect(await rolesOf("linus@example.test")).toEqual({ "acme-review": "viewer" })
})
it("gives exactly one of the organizations a tenant brand", async () => {
const { app } = await seeded()
const grace = (
await app.data
.global(users)
.list({ where: [{ field: "email", op: "eq", value: "grace@example.test" }], limit: 1 })
).items[0]
const call = app.caller({ userId: grace?.id ?? "" })
expect(await call.callByName("branding.brand.get", { org: "northwind" })).toMatchObject({
displayName: "Northwind",
primary: "#1a1a8c",
accent: "#f4c430",
})
expect(await call.callByName("branding.brand.get", { org: "acme-review" })).toMatchObject({
displayName: null,
primary: null,
})
})
it("puts a project and two notes where a module's pages can show them", async () => {
const { app } = await seeded()
const ada = (
await app.data.global(users).list({ where: [{ field: "email", op: "eq", value: "ada@example.test" }], limit: 1 })
).items[0]
const call = app.caller({ userId: ada?.id ?? "" })
const listed = (await call.callByName("notes.note.list", {
org: "acme-review",
workspace: "general",
project: "notes",
})) as { notes: Array<{ title: string }> }
expect(listed.notes.map((note) => note.title).sort()).toEqual(["Roles", "Welcome"])
})
it("creates the people as a sign-in does, under the dev-login issuer, so the dev sign-in lets them in", async () => {
const { app } = await seeded()
const rows = await app.data.global(users).list({ limit: 10 })
expect(rows.items.map((user) => [user.issuer, user.subject, user.email]).sort()).toEqual(
DEMO_PEOPLE.map((person) => [DEV_LOGIN_ISSUER, person.email, person.email]).sort(),
)
})
it("changes nothing when it runs again", async () => {
const { app, deps, summary } = await seeded()
expect(summary.created.length).toBeGreaterThan(5)
const before = JSON.stringify({
users: (await app.data.global(users).list({ limit: 50 })).items.map((user) => [user.email, user.version]),
outbox: (await app.data.outbox.pending(500)).length,
})
const again = await seedDemo(deps)
expect(again.created).toEqual([])
expect(
JSON.stringify({
users: (await app.data.global(users).list({ limit: 50 })).items.map((user) => [user.email, user.version]),
outbox: (await app.data.outbox.pending(500)).length,
}),
).toBe(before)
})
it("names the organizations the way the README and the sign-in help do", () => {
expect(DEMO_ORGANIZATIONS.acme.slug).toBe("acme-review")
expect(DEMO_ORGANIZATIONS.northwind.brand.displayName).toBe("Northwind")
})
})
+230
View File
@@ -0,0 +1,230 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The brand as a share image and an email show it (spec §10.2, §10.3): the app brand's name and logo, or an
// organization's, drawn through the kit's logo checks. Every logo here goes the way it goes in production: an
// organization's through the caller (so a stored, normalised file), the app's from a file of a public folder.
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import {
type BrandView,
type ComposedEmail,
composeEmail,
type MailTransport,
renderShareImage,
type ShareBrand,
sendEmail,
shareBrand,
} from "@app/modules/branding"
import { afterEach, describe, expect, it } from "vitest"
import { appLogoSource, readersFor } from "../apps/web/src/server/brand-assets"
import { closeApps, makeDocumentsApp, type SeededProject } from "./support/documents-app.js"
import { dimensions, markPng, pixelAt } from "./support/images.js"
const directories: string[] = []
afterEach(async () => {
await closeApps()
for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true })
})
const TEAL = [0x80, 0xcd, 0xc6, 255]
const svg = (fill: string) =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 80"><rect width="300" height="80" fill="${fill}"/></svg>`
/** A public folder holding the app brand's two logos: a blue rectangle for light, a red one for dark. */
function publicFolder(): string {
const dir = mkdtempSync(join(tmpdir(), "share-public-"))
directories.push(dir)
writeFileSync(join(dir, "app-light.svg"), svg("#3366aa"))
writeFileSync(join(dir, "app-dark.svg"), svg("#aa3333"))
return dir
}
const appIdentity = { name: "Example App", logo: { light: "/app-light.svg", dark: "/app-dark.svg" } }
const empty: BrandView = { displayName: null, primary: null, accent: null, logoLight: null, logoDark: null }
async function acme(): Promise<{ project: SeededProject }> {
const app = await makeDocumentsApp({
document: { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] },
})
return { project: await app.seedProject("acme", "owner@acme.test") }
}
const upload = (project: SeededProject, variant: "light" | "dark", bytes: Uint8Array) =>
project.call.callByName("branding.logo.set", {
org: project.orgSlug,
variant,
contentType: "image/png",
body: Buffer.from(bytes).toString("base64"),
})
// The logo box of the share image: 300 x 80 at (80, 80); a wide logo's middle is at (230, 120).
const logoCentre = (png: Uint8Array) => pixelAt(png, 230, 120)
describe("the app's own logo", () => {
it("is the kit's mark when the public folder holds no file of that name", async () => {
const { contentType, bytes } = await appLogoSource("/favicon.png", new URL(".", import.meta.url).pathname)
expect(contentType).toBe("image/png")
expect(Buffer.from(bytes).subarray(1, 4).toString()).toBe("PNG")
})
})
describe("the app brand's logo from the public folder", () => {
it("is read from a file of the folder and from nowhere else", async () => {
const dir = publicFolder()
expect(await appLogoSource("/app-dark.svg", dir)).toMatchObject({ contentType: "image/svg+xml" })
await expect(appLogoSource("/../secret.svg", dir)).rejects.toThrow(/public folder/)
await expect(appLogoSource("/app.exe", dir)).rejects.toThrow(/PNG, WebP or SVG/)
await expect(appLogoSource("/missing.svg", dir)).rejects.toThrow()
})
})
describe("the share image", () => {
it("draws the app brand's logo for the dark page when there is no organization", async () => {
const dir = publicFolder()
const share = await shareBrand({
app: appIdentity,
tenant: null,
readers: { appLogo: (path) => appLogoSource(path, dir), tenantLogo: () => Promise.reject(new Error("none")) },
})
expect(share.name).toBe("Example App")
const png = await renderShareImage(share, { title: share.name })
expect(dimensions(png)).toEqual({ width: 1200, height: 630 })
expect(logoCentre(png)).toEqual([0xaa, 0x33, 0x33, 255])
})
it("draws an organization's own logo and display name, and the logo is the stored one", async () => {
const { project } = await acme()
await project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: "Acme Review" })
await upload(project, "dark", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(share.name).toBe("Acme Review")
expect(share.logo.contentType).toBe("image/png")
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("draws the only logo the organization has when it is the light one, because the page is dark", async () => {
const { project } = await acme()
await upload(project, "light", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
expect(brand.logoDark).toBeNull()
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(share.name).toBe("Acme Ltd")
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("is not stopped by a missing app logo file when the organization has a good logo of its own", async () => {
const { project } = await acme()
await upload(project, "dark", markPng(2000, 500))
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand },
readers: readersFor(project.call, project.orgSlug, mkdtempSync(join(tmpdir(), "share-nofile-"))),
})
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL)
})
it("falls back to the app brand's logo for an organization with none", async () => {
const { project } = await acme()
const share = await shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand: empty },
readers: readersFor(project.call, project.orgSlug, publicFolder()),
})
expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual([0xaa, 0x33, 0x33, 255])
})
it("draws the app brand's logo instead when the chosen one cannot be drawn, and does not fail", async () => {
const dir = publicFolder()
const broken: ShareBrand = {
name: "Acme Ltd",
logo: { contentType: "image/png", bytes: new TextEncoder().encode("<svg/>") },
fallback: await appLogoSource("/app-dark.svg", dir),
}
expect(logoCentre(await renderShareImage(broken, { title: broken.name }))).toEqual([0xaa, 0x33, 0x33, 255])
const refused: ShareBrand = { ...broken, fallback: broken.logo }
await expect(renderShareImage(refused, { title: "x" })).rejects.toThrow(/logo cannot be used/)
})
})
describe("the email", () => {
const recorded = () => {
const sent: Array<ComposedEmail & { to: string }> = []
const transport: MailTransport = {
async send(message) {
sent.push(message)
},
}
return { sent, transport }
}
async function shareOf(logo: Uint8Array | null, dir = publicFolder()): Promise<ShareBrand> {
const { project } = await acme()
if (logo) await upload(project, "dark", logo)
const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView
return shareBrand({
app: appIdentity,
tenant: { name: "Acme Ltd", brand: { ...brand, displayName: "Acme Review" } },
readers: readersFor(project.call, project.orgSlug, dir),
})
}
it("carries the organization's name and its logo as an inline attachment, and names no host", async () => {
const mail = await composeEmail(await shareOf(markPng(2000, 500)), {
subject: "You are invited",
paragraphs: ["Hello,", "Open the app to accept."],
})
expect(mail.subject).toBe("You are invited - Acme Review")
expect(mail.html).toContain('alt="Acme Review"')
expect(mail.html).toContain('src="cid:brand-logo"')
expect(mail.html).not.toMatch(/https?:/)
expect(mail.html.toLowerCase()).not.toContain("conusai")
expect(mail.text).toBe("Hello,\n\nOpen the app to accept.\n\nAcme Review\n")
const [logo] = mail.attachments
expect(logo).toMatchObject({ cid: "brand-logo", contentType: "image/png" })
expect(dimensions(logo?.content ?? new Uint8Array())).toEqual({ width: 420, height: 105 })
expect(pixelAt(logo?.content ?? new Uint8Array(), 210, 52)).toEqual(TEAL)
})
it("uses the app brand's logo for an organization with none", async () => {
const mail = await composeEmail(await shareOf(null), { subject: "Hi", paragraphs: ["x"] })
const content = mail.attachments[0]?.content ?? new Uint8Array()
expect(dimensions(content).width).toBe(420)
expect(pixelAt(content, 210, 52)).toEqual([0xaa, 0x33, 0x33, 255])
})
it("escapes what it is given as text, and refuses a subject that is more than one line", async () => {
const share = await shareOf(null)
const mail = await composeEmail(share, { subject: "x", paragraphs: ['<script>alert("1")</script> & more'] })
expect(mail.html).toContain("&#60;script&#62;alert(&#34;1&#34;)&#60;/script&#62; &#38; more")
expect(mail.html).not.toContain("<script>")
await expect(composeEmail(share, { subject: "x\r\nBcc: someone@example.test", paragraphs: ["x"] })).rejects.toThrow(
/one line/,
)
await expect(composeEmail(share, { subject: "x", paragraphs: [] })).rejects.toThrow(/paragraph/)
})
it("is handed to the transport with one address, and refuses an address that carries a header", async () => {
const share = await shareOf(null)
const { sent, transport } = recorded()
await sendEmail(transport, share, "member@acme.test", { subject: "Welcome", paragraphs: ["Hello"] })
expect(sent).toHaveLength(1)
expect(sent[0]).toMatchObject({ to: "member@acme.test", subject: "Welcome - Acme Review" })
for (const to of ["a@b.test\r\nBcc: c@d.test", "a@b.test, c@d.test", "no-address", "a b@c.test"]) {
await expect(sendEmail(transport, share, to, { subject: "x", paragraphs: ["x"] })).rejects.toThrow(
/one email address/,
)
}
expect(sent).toHaveLength(1)
})
})
+93
View File
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What a loader does with a refusal on TanStack Start (F10): a real 302, 404 or 403, set before anything
// streams. Start has no `forbidden()`, so the 403 is a not-found with `data.forbidden` plus the header the
// server entry turns into the status.
import { AppError } from "@conusai-kit/core"
import { isNotFound, isRedirect } from "@tanstack/react-router"
import { beforeEach, describe, expect, it, vi } from "vitest"
const start = vi.hoisted(() => ({
setResponseStatus: vi.fn<(status: number) => void>(),
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
}))
vi.mock("@tanstack/react-start/server", () => start)
const { orFallback, PAGE_STATUS_HEADER, pageForbidden, pageNotFound, signInHref, toSignIn } = await import(
"../apps/web/src/server/guard"
)
const refusal = (status: number): AppError =>
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
const thrownBy = async (run: () => unknown): Promise<unknown> => {
try {
await run()
} catch (error) {
return error
}
throw new Error("it did not throw")
}
beforeEach(() => {
start.setResponseStatus.mockClear()
start.setResponseHeader.mockClear()
})
describe("signInHref", () => {
it("names where to come back to, and nothing for the home page", () => {
expect(signInHref("/acme/general/demo/notes?x=1")).toBe(
"/auth/sign-in?return_to=%2Facme%2Fgeneral%2Fdemo%2Fnotes%3Fx%3D1",
)
expect(signInHref("/")).toBe("/auth/sign-in")
expect(signInHref("/", "denied")).toBe("/auth/sign-in?error=denied")
})
})
describe("the status pages", () => {
it("answers a signed-out visitor with a 302 to sign in and back", async () => {
const error = (await thrownBy(() => toSignIn("/a/b/c/notes"))) as { options: { href: string; statusCode: number } }
expect(isRedirect(error)).toBe(true)
expect(error.options.href).toBe("/auth/sign-in?return_to=%2Fa%2Fb%2Fc%2Fnotes")
expect(error.options.statusCode).toBe(302)
})
it("answers not found with 404, in the status and in the header the server entry applies", async () => {
expect(isNotFound(await thrownBy(() => pageNotFound()))).toBe(true)
expect(start.setResponseStatus).toHaveBeenCalledWith(404)
expect(start.setResponseHeader).toHaveBeenCalledWith(PAGE_STATUS_HEADER, "404")
})
it("answers forbidden with 403 and marks the not-found as the forbidden page", async () => {
const error = (await thrownBy(() => pageForbidden())) as { data: unknown }
expect(isNotFound(error)).toBe(true)
expect(error.data).toEqual({ forbidden: true })
expect(start.setResponseHeader).toHaveBeenCalledWith(PAGE_STATUS_HEADER, "403")
})
})
describe("orFallback", () => {
it("passes an answer through", async () => {
await expect(orFallback("/here", async () => "rows")).resolves.toBe("rows")
expect(start.setResponseStatus).not.toHaveBeenCalled()
})
it.each([
[404, "404"],
[403, "403"],
])("turns a procedure's %i into that page", async (status, header) => {
const error = await thrownBy(() => orFallback("/here", () => Promise.reject(refusal(status))))
expect(isNotFound(error)).toBe(true)
expect(start.setResponseHeader).toHaveBeenCalledWith(PAGE_STATUS_HEADER, header)
})
it("turns a procedure's 401 into the way to sign in", async () => {
expect(isRedirect(await thrownBy(() => orFallback("/here", () => Promise.reject(refusal(401)))))).toBe(true)
})
it("leaves anything else to the error boundary", async () => {
const fault = new Error("database down")
expect(await thrownBy(() => orFallback("/here", () => Promise.reject(fault)))).toBe(fault)
expect(await thrownBy(() => orFallback("/here", () => Promise.reject(refusal(500))))).toBeInstanceOf(AppError)
expect(start.setResponseStatus).not.toHaveBeenCalled()
})
})
+193
View File
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// What a route loader reads (F10). `pageContext` and `permissionsIn` are kept per request, because React's
// `cache` memoizes nothing outside server components, and `modulePageData` is the one module route's
// loader: signed out is a redirect, every refusal is a real status, and a module's data passes through.
import { AppError } from "@conusai-kit/core"
import { isNotFound, isRedirect } from "@tanstack/react-router"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
requestContext: vi.fn(),
callByName: vi.fn(),
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
load: vi.fn(),
call: vi.fn(),
}))
vi.mock("@tanstack/react-start/server", () => ({
getRequest: () => {
throw new Error("the tests pass their request")
},
setResponseStatus: vi.fn(),
setResponseHeader: mocks.setResponseHeader,
}))
vi.mock("../apps/web/src/server/request", () => ({ requestContext: mocks.requestContext }))
vi.mock("../apps/web/src/server/app", () => ({
webRuntime: () => ({
observe: { config: { errors: false } },
selfServiceOrganizations: "members",
registry: {
modules: [{ id: "notes" }, { id: "unpaged" }],
nav: [{ module: "notes", scope: "project", href: "notes", label: "workspace.projects" }],
},
env: { deployment: {} },
}),
}))
vi.mock("../apps/web/src/server/tenancy-calls", () => ({ mayCreateOrganization: () => false }))
vi.mock("../apps/web/src/server/platform-theme", () => ({
organizationThemeStyle: async () => undefined,
organizationIdentity: async () => ({}),
}))
vi.mock("@conusai-kit/host/http", () => ({ csrfSessionId: () => "session", csrfToken: () => "token" }))
vi.mock("@app/modules/pages", () => ({ modulePages: { notes: async () => ({ load: mocks.load }) } }))
const { modulePageData, pageContext, permissionsIn, projectLayoutData, shellData } = await import(
"../apps/web/src/server/page"
)
const person = { actor: { kind: "user" } }
const signedIn = () =>
mocks.requestContext.mockResolvedValue({
caller: { call: mocks.call, callByName: mocks.callByName },
container: {},
principal: person,
user: { email: "ada@example.test", displayName: "Ada", locale: "en" },
})
const signedOut = () =>
mocks.requestContext.mockResolvedValue({
caller: { call: mocks.call, callByName: mocks.callByName },
container: {},
principal: null,
})
const request = (): Request => new Request("http://app.test/a/b/c/notes", { headers: { "x-path": "/a/b/c/notes" } })
const params = { org: "a", workspace: "b", project: "c", module: "notes" }
const refusal = (status: number): AppError =>
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
beforeEach(() => {
vi.clearAllMocks()
mocks.callByName.mockResolvedValue({ permissions: ["notes.note.read"], revision: "r1" })
mocks.call.mockResolvedValue({})
mocks.load.mockResolvedValue({ kind: "list", title: "Notes" })
})
describe("the page context", () => {
it("is built once per request, and again for the next one", async () => {
signedIn()
const first = request()
await Promise.all([pageContext(first), pageContext(first)])
expect(mocks.requestContext).toHaveBeenCalledOnce()
await pageContext(request())
expect(mocks.requestContext).toHaveBeenCalledTimes(2)
})
it("reads the permissions of a scope once per request", async () => {
signedIn()
const current = request()
await Promise.all([permissionsIn({ org: "a" }, current), permissionsIn({ org: "a" }, current)])
expect(mocks.callByName).toHaveBeenCalledOnce()
await permissionsIn({ org: "a", workspace: "b" }, current)
expect(mocks.callByName).toHaveBeenCalledTimes(2)
})
})
describe("the shell's loader data", () => {
it("is plain values with its labels translated, and nothing a browser must not hold", async () => {
vi.stubEnv("CONUSAI_PUBLIC_URL", "https://app.example.test")
signedIn()
const shell = await shellData(request())
expect(JSON.parse(JSON.stringify(shell))).toEqual(shell)
expect(shell.signedIn).toBe(true)
expect(shell.labels.signIn.length).toBeGreaterThan(0)
expect(shell.brand.style).toBeTypeOf("object")
expect(shell.publicUrl).toBe("https://app.example.test")
expect(JSON.stringify(shell)).not.toContain("ada@example.test")
})
})
describe("the module route's loader", () => {
const thrown = (run: Promise<unknown>): Promise<unknown> =>
run.then(
() => "resolved",
(error: unknown) => error,
)
it("sends a signed-out visitor to sign in, before reading anything", async () => {
signedOut()
expect(isRedirect(await thrown(modulePageData(params, request())))).toBe(true)
expect(mocks.call).not.toHaveBeenCalled()
expect(mocks.load).not.toHaveBeenCalled()
})
it.each([
["no registered module", { ...params, module: "nothing" }],
["a registered module with no page", { ...params, module: "unpaged" }],
["a name every object has", { ...params, module: "constructor" }],
])("answers 404 for %s", async (_name, wanted) => {
signedIn()
expect(isNotFound(await thrown(modulePageData(wanted, request())))).toBe(true)
expect(mocks.load).not.toHaveBeenCalled()
})
it.each([
[404, undefined],
[403, { forbidden: true }],
])("turns the module's own %i into that page", async (status, data) => {
signedIn()
mocks.load.mockRejectedValue(refusal(status))
const error = (await thrown(modulePageData(params, request()))) as { data?: unknown }
expect(isNotFound(error)).toBe(true)
expect(error.data).toEqual(data)
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", String(status))
})
it("leaves a fault to the error boundary", async () => {
signedIn()
const fault = new Error("database down")
mocks.load.mockRejectedValue(fault)
expect(await thrown(modulePageData(params, request()))).toBe(fault)
})
it("runs the module's loader with the slugs and the rest of the path", async () => {
signedIn()
const page = await modulePageData({ ...params, _splat: "note-1/" }, request())
expect(mocks.load.mock.calls[0]?.[0]).toMatchObject({
slugs: { org: "a", workspace: "b", project: "c" },
segments: ["note-1"],
})
expect(page).toMatchObject({
module: "notes",
title: "Projects · Example App",
segments: ["note-1"],
data: { kind: "list", title: "Notes" },
})
})
})
describe("the project route's loader", () => {
const slugs = { org: "a", workspace: "b", project: "c" }
const refused = (run: Promise<unknown>): Promise<unknown> => run.catch((error: unknown) => error)
it("sends a signed-out visitor to sign in, before it reads anything", async () => {
signedOut()
expect(isRedirect(await refused(projectLayoutData(slugs, request())))).toBe(true)
expect(mocks.call).not.toHaveBeenCalled()
})
it("answers 404 for a project of another tenant", async () => {
signedIn()
mocks.call.mockRejectedValue(refusal(404))
expect(isNotFound(await refused(projectLayoutData(slugs, request())))).toBe(true)
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
})
it("returns the project's scope and the person's permissions there, the seed of useCan", async () => {
signedIn()
expect(await projectLayoutData(slugs, request())).toMatchObject({
scope: slugs,
seed: expect.objectContaining({ permissions: ["notes.note.read"] }),
project: { id: slugs.project },
})
})
})
+43
View File
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Start has no `onRequestError`: the loaders and the server entry report what they leave unhandled, and a
// redirect or a not-found the app raised on purpose is control flow.
import { notFound, redirect } from "@tanstack/react-router"
import { afterEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({ current: undefined as unknown }))
vi.mock("@app/config", () => ({ currentObservability: () => mocks.current }))
const { reportFailure } = await import("../apps/web/src/server/report")
const context = { routePath: "/$org", routeType: "loader" }
const observer = () => ({
captureException: vi.fn(),
log: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
})
const request = new Request("http://app.test/acme/notes?x=1", { headers: { traceparent: "t" } })
afterEach(() => {
mocks.current = undefined
})
describe("a failed request's report", () => {
it("reports a fault with the route, and says nothing of a redirect or a not-found", () => {
const observe = observer()
mocks.current = observe
reportFailure(notFound(), request, context)
reportFailure(redirect({ href: "/" }), request, context)
expect(observe.captureException).not.toHaveBeenCalled()
const fault = new Error("fault")
reportFailure(fault, request, context)
expect(observe.captureException).toHaveBeenCalledWith(fault, { route: "/$org", kind: "loader" })
expect(observe.log.error).toHaveBeenCalledOnce()
expect(observe.log.error.mock.calls[0]?.[1]).toMatchObject({ "url.path": "/acme/notes" })
})
it("does nothing in a process that started no observability", () => {
expect(() => reportFailure(new Error("fault"), request, context)).not.toThrow()
})
})
+329
View File
@@ -0,0 +1,329 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The loaders of the pages outside an organization (M6b): each answers a signed-out visitor, a refused read
// and a redirect with its real status, and returns data that survives the trip to the browser.
import { AppError } from "@conusai-kit/core"
import { isNotFound, isRedirect } from "@tanstack/react-router"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
pageContext: vi.fn(),
call: vi.fn(),
config: vi.fn(),
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
cookie: "",
}))
vi.mock("@tanstack/react-start/server", () => ({
getRequest: () => new Request("http://app.test/", { headers: { cookie: mocks.cookie } }),
setResponseStatus: vi.fn(),
setResponseHeader: mocks.setResponseHeader,
}))
vi.mock("../apps/web/src/server/page", () => ({ pageContext: mocks.pageContext }))
vi.mock("@conusai-kit/auth", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
loadAuthConfig: mocks.config,
}))
vi.mock("../apps/web/src/server/better-auth", () => ({
MIN_PASSWORD_LENGTH: 12,
betterAuthFormLabels: () => ({ submit: "Go" }),
}))
vi.mock("@app/modules/account/contract", async (importOriginal) => ({
// The module list behind the app's catalogues reads the account module's entities.
...(await importOriginal<Record<string, unknown>>()),
ACCOUNT_PATH: "/settings/account",
accountDeletionStatus: {},
}))
vi.mock("@app/modules/account/ui", () => ({
analyticsLabels: () => ({}),
deletionLabels: () => ({}),
exportLabels: () => ({}),
pageLabels: () => ({ title: "Account" }),
}))
vi.mock("@conusai-kit/tenancy", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
meGet: "me",
invitationRead: "invitation.read",
}))
vi.mock("@conusai-kit/tenancy/ui", () => ({ builtInRoleName: (_i18n: unknown, role: string) => `kit:${role}` }))
const { signInSearch } = await import("../apps/web/src/ui/sign-in-search")
const { accountData, homeData, invitationData, membersEntryData, onboardingData, signInData } = await import(
"../apps/web/src/server/account-pages"
)
const { signUpData } = await import("../apps/web/src/server/account-pages") // kit:convex-better-auth
const i18n = { message: (key: string) => key, locale: "en" }
const context = (extra: object) => ({ i18n, caller: { call: mocks.call }, path: "/here", ...extra })
const person = (extra: object = {}) => mocks.pageContext.mockResolvedValue(context({ signedIn: true, ...extra }))
const visitor = () => mocks.pageContext.mockResolvedValue(context({ signedIn: false }))
const refusal = (status: number): AppError =>
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
const organizations = (...slugs: string[]) => ({
organizations: slugs.map((slug) => ({ slug, name: slug, role: "owner" })),
})
const thrown = (run: Promise<unknown>): Promise<unknown> =>
run.then(
() => {
throw new Error("expected a refusal")
},
(error: unknown) => error,
)
const hrefOf = (error: unknown): string | undefined => (error as { options?: { href?: string } }).options?.href
beforeEach(() => {
vi.clearAllMocks()
mocks.cookie = ""
mocks.config.mockReturnValue({ provider: "oidc", devLogin: true })
})
describe("the entry page", () => {
it("offers a visitor the way in", async () => {
visitor()
mocks.config.mockReturnValue({ provider: "oidc", oidc: {} })
const data = await homeData()
expect(data).toMatchObject({
signedIn: false,
signIn: {
provider: { href: "/auth/sign-in/start?ui_locales=en", label: "app.signIn" },
signUp: { href: "/auth/sign-in/start?ui_locales=en&intent=sign-up", label: "signin.createAccount" },
},
})
expect(mocks.call).not.toHaveBeenCalled()
})
it("takes a person with one organization straight to it", async () => {
person({ mayCreateOrganization: true })
mocks.call.mockResolvedValue(organizations("acme"))
expect(hrefOf(await thrown(homeData()))).toBe("/acme")
})
it("takes a person with none to onboarding, or says why when they may not found one", async () => {
person({ mayCreateOrganization: true })
mocks.call.mockResolvedValue(organizations())
expect(hrefOf(await thrown(homeData()))).toBe("/onboarding")
person({ mayCreateOrganization: false })
const data = await homeData()
expect(data).toMatchObject({ signedIn: true, emptyHint: "app.askForInvitation" })
expect(data).not.toHaveProperty("create")
})
it("lists several organizations with their role translated", async () => {
person({ mayCreateOrganization: false })
mocks.call.mockResolvedValue(organizations("acme", "north"))
expect(await homeData()).toEqual(
expect.objectContaining({
organizations: [
{ slug: "acme", name: "acme", role: "kit:owner" },
{ slug: "north", name: "north", role: "kit:owner" },
],
}),
)
})
})
describe("onboarding", () => {
it("sends a visitor to sign in and back", async () => {
visitor()
expect(hrefOf(await thrown(onboardingData()))).toBe("/auth/sign-in?return_to=%2Fhere")
})
it("is the forbidden page, 403, for a person who may not found an organization", async () => {
person({ mayCreateOrganization: false })
const error = (await thrown(onboardingData())) as { data?: unknown }
expect(isNotFound(error)).toBe(true)
expect(error.data).toEqual({ forbidden: true })
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "403")
})
it("names the first workspace in the deployment's language, whatever the founder's is", async () => {
person({ mayCreateOrganization: true, deploymentI18n: { message: (key: string) => `lt:${key}` } })
expect((await onboardingData()).workspace).toEqual({
name: "lt:onboarding.workspaceName",
slug: "lt:onboarding.workspaceSlug",
})
})
})
describe("/members", () => {
it.each([
[[], "/"],
[["acme"], "/acme/members"],
])("sends a person with organizations %j to %s", async (slugs, href) => {
person()
mocks.call.mockResolvedValue(organizations(...slugs))
const error = await thrown(membersEntryData())
expect(isRedirect(error)).toBe(true)
expect(hrefOf(error)).toBe(href)
})
it("lists several organizations", async () => {
person()
mocks.call.mockResolvedValue(organizations("a", "b"))
expect((await membersEntryData()).organizations.map(({ slug }) => slug)).toEqual(["a", "b"])
})
})
describe("the account page", () => {
it("uses only a remembered organization the person belongs to", async () => {
person()
mocks.call.mockImplementation(async (procedure: unknown) =>
procedure === "me"
? { displayName: "Ada", email: "ada@example.test", ...organizations("a", "b") }
: { interactive: true, recentSignIn: false, deletion: null },
)
mocks.cookie = "app_workspace=b%2Fgeneral"
expect(hrefOf(await thrown(accountData(true)))).toBe("/b/settings/account")
mocks.cookie = "app_workspace=other%2Fgeneral"
expect((await accountData(true)).page.title).toBe("Account")
mocks.cookie = "app_workspace=b%2Fgeneral%2Fextra"
expect((await accountData(true)).page.title).toBe("Account")
})
it("uses the only organization when there is no workspace preference", async () => {
person()
mocks.call.mockImplementation(async (procedure: unknown) =>
procedure === "me"
? { displayName: "Ada", email: "ada@example.test", ...organizations("a") }
: { interactive: true, recentSignIn: false, deletion: null },
)
expect(hrefOf(await thrown(accountData(true)))).toBe("/a/settings/account")
})
it("sends a person back through the dev login to sign in again, when that is the deployment's way in", async () => {
person()
mocks.call.mockImplementation(async (procedure: unknown) =>
procedure === "me"
? { displayName: "Ada", email: "ada@example.test" }
: { interactive: true, recentSignIn: false, deletion: null },
)
const data = await accountData()
expect(data.deletion.reauthHref).toBe("/auth/dev-login?email=ada%40example.test&return_to=%2Fsettings%2Faccount")
expect(() => JSON.stringify(data)).not.toThrow()
})
})
describe("the sign-in pages", () => {
it("take a string from the query or nothing: the router parses `?return_to=123` as a number", () => {
expect(signInSearch({ return_to: 123, error: true })).toEqual({})
expect(signInSearch({ return_to: "/members", error: "denied" })).toEqual({ return_to: "/members", error: "denied" })
})
it("sends somebody already in where they were going, and only inside this app", async () => {
person()
expect(hrefOf(await thrown(signInData({ return_to: "/members" })))).toBe("/members")
expect(hrefOf(await thrown(signInData({ return_to: "https://evil.example/" })))).toBe("/")
})
it("keeps somebody who was sent back with an error, and says what went wrong", async () => {
person()
expect(await signInData({ error: "denied" })).toMatchObject({ error: "signin.error.denied" })
})
it("offers the identity provider when there is one, with where to come back to", async () => {
visitor()
mocks.config.mockReturnValue({ provider: "oidc", oidc: {} })
const data = await signInData({ return_to: "/members" })
expect(data.provider?.href).toBe("/auth/sign-in/start?ui_locales=en&return_to=%2Fmembers")
expect(data).not.toHaveProperty("dev")
})
it("offers the development login with no provider, and says so when there is neither", async () => {
visitor()
expect((await signInData({ return_to: "/members" })).dev?.returnTo).toBe("/members")
mocks.config.mockReturnValue({ provider: "oidc", devLogin: false })
expect(await signInData({})).toMatchObject({ notConfigured: "signin.notConfigured" })
})
// kit:convex-better-auth:begin
it("has no sign-up page in an OIDC deployment", async () => {
visitor()
expect(isNotFound(await thrown(signUpData({})))).toBe(true)
})
it("offers the account form with the Convex Better Auth adapter", async () => {
visitor()
mocks.config.mockReturnValue({ provider: "convex-better-auth" })
expect((await signInData({})).betterAuth?.toSignUp.href).toBe("/auth/sign-up")
expect(await signUpData({ return_to: "/members" })).toMatchObject({
returnTo: "/members",
toSignIn: { href: "/auth/sign-in?return_to=%2Fmembers" },
})
})
// kit:convex-better-auth:end
})
describe("an invitation", () => {
const open = {
invitationId: "inv-1",
state: "open",
email: "guest@example.test",
organization: { slug: "acme", name: "Acme" },
role: "member",
roleKey: "editor",
expiresAt: "2026-11-10T09:00:00.000Z",
}
it("tells a visitor who it is for and until when, and offers Create your account and Sign in", async () => {
mocks.pageContext.mockResolvedValue(context({ signedIn: false, path: "/invitations/acme/token", theme: "dark" }))
mocks.call.mockResolvedValue(open)
const data = await invitationData({ org: "acme", token: "token" })
expect(mocks.call).toHaveBeenCalledWith("invitation.read", { org: "acme", token: "token" })
expect(data).toMatchObject({ state: "open", lead: "invite.lead", expires: "invite.expires" })
expect(data.start).toEqual({
org: "acme",
token: "token",
locale: "en",
theme: "dark",
createAccount: "invite.createAccount",
signIn: "invite.haveAccount",
})
expect(data.signedIn).toBeUndefined()
})
it("says plainly that a link was used, revoked or has expired, and that an unknown one is not valid", async () => {
visitor()
for (const state of ["used", "revoked", "expired"]) {
mocks.call.mockResolvedValueOnce({ ...open, state })
const data = await invitationData({ org: "acme", token: "token" })
expect(data).toMatchObject({ state, lead: `invite.error.${state}` })
expect(data.start).toBeUndefined()
}
mocks.call.mockRejectedValueOnce(refusal(404))
expect(await invitationData({ org: "acme", token: "token" })).toMatchObject({
state: "invalid",
lead: "invite.error.unknown",
})
})
it("offers the invited address the accept button, and anybody else only who it is for", async () => {
person({ user: { email: "Guest@Example.test" } })
mocks.call.mockResolvedValue(open)
const mine = await invitationData({ org: "acme", token: "token" })
expect(mine.signedIn?.labels?.accept).toBe("invite.accept")
expect(mine.signedIn?.otherAccount).toBeUndefined()
person({ user: { email: "ada@example.test" } })
const theirs = await invitationData({ org: "acme", token: "token" })
expect(theirs.signedIn?.labels).toBeUndefined()
expect(theirs.signedIn?.otherAccount).toBe("invite.otherAccount")
})
it("asks a visitor to sign in first where the sign-in takes no invitation", async () => {
mocks.config.mockReturnValue({ provider: "convex-better-auth", devLogin: false })
mocks.pageContext.mockResolvedValue(context({ signedIn: false, path: "/invitations/acme/token" }))
mocks.call.mockResolvedValue(open)
expect((await invitationData({ org: "acme", token: "token" })).signInFirst?.href).toBe(
"/auth/sign-in?return_to=%2Finvitations%2Facme%2Ftoken",
)
})
})
describe("a read refused", () => {
it("is the page that belongs to it", async () => {
person()
mocks.call.mockRejectedValue(refusal(401))
expect(isRedirect(await thrown(homeData()))).toBe(true)
mocks.call.mockRejectedValue(refusal(404))
expect(isNotFound(await thrown(homeData()))).toBe(true)
})
})
+407
View File
@@ -0,0 +1,407 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The route files of the Next tree's replacement (M6b, M6c): the table in apps/web/ROUTES.md has a file for
// every row, what reaches the browser imports no server code, and the loaders under an organization answer
// every refusal with its status.
import { existsSync, readdirSync, readFileSync } from "node:fs"
import { AppError } from "@conusai-kit/core"
import { isNotFound, isRedirect } from "@tanstack/react-router"
import { beforeEach, describe, expect, it, vi } from "vitest"
const web = new URL("../apps/web/", import.meta.url)
const routes = new URL("src/routes/", web)
const mocks = vi.hoisted(() => ({
pageContext: vi.fn(),
holds: vi.fn(),
requirePermission: vi.fn(),
permissionsIn: vi.fn(),
call: vi.fn(),
requireSection: vi.fn(),
membersLoad: vi.fn(),
invitationsLoad: vi.fn(),
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
nav: vi.fn((): unknown[] => []),
request: vi.fn(() => new Request("http://app.test/")),
singleOrganization: vi.fn(() => false),
selected: new Set(["documents", "knowledge"]),
}))
vi.mock("@tanstack/react-start/server", () => ({
getRequest: () => mocks.request(),
setResponseStatus: vi.fn(),
setResponseHeader: mocks.setResponseHeader,
}))
vi.mock("../apps/web/src/server/page", () => ({
holds: mocks.holds,
pageContext: mocks.pageContext,
permissionsIn: mocks.permissionsIn,
requirePermission: mocks.requirePermission,
}))
vi.mock("../apps/web/src/server/app", () => ({
webRuntime: () => ({
env: { deployment: {} },
observe: { log: { warn: vi.fn() } },
registry: { nav: mocks.nav() },
singleOrganization: mocks.singleOrganization(),
}),
}))
vi.mock("../apps/web/src/server/project-entries", () => ({
PROJECT_ENTRIES: [{ href: "archive", label: "archive.title", module: "archive" }],
PROJECT_MODULE_KEYS: ["archive"],
}))
vi.mock("@app/config", () => ({ PROFILE: "postgres" }))
vi.mock("@app/modules", () => ({
moduleSelected: (id: string) => mocks.selected.has(id),
moduleAdopted: (id: string) => mocks.selected.has(id),
appModules: [
{
id: "tenancy",
version: "1.0.0",
procedures: [{ name: "a function that must not reach the browser" }],
ui: {
nav: [
{ scope: "org", label: "tenancy.nav.members", href: "members" },
{ scope: "org", label: "tenancy.nav.settings", href: "settings", permission: "tenancy.org.manage" },
],
},
},
{ id: "worker-only", version: "1.0.0" },
],
}))
vi.mock("@app/modules/branding", () => ({
brandGet: "brand",
logoUrl: (path: string, size: string) => `${path}?s=${size}`,
tenantLogo: (brand: { logoLight: string | null }) => (brand.logoLight ? { path: brand.logoLight } : null),
tenantStyle: () => ({ style: { "--tenant": "1" } }),
}))
vi.mock("@app/modules/branding/ui", () => ({ brandFormLabels: () => ({}) }))
// kit:documents:begin
vi.mock("@app/modules/documents", () => ({
documentGet: "documentGet",
documentList: "documentList",
READABLE_CONTENT_TYPES: ["application/pdf"],
}))
vi.mock("@app/modules/documents/ui", () => ({
exportLabels: () => ({}),
listLabels: () => ({}),
reviewLabels: () => ({}),
uploadLabels: () => ({}),
}))
// kit:documents:end
vi.mock("@app/modules/knowledge", () => ({ settingsGet: "settingsGet" })) // kit:knowledge
vi.mock("@conusai-kit/brand", () => ({ tokens: { color: { dark: { accent: "#fff" } } } }))
vi.mock("@conusai-kit/host/http", () => ({ isOwnedBy: () => true }))
vi.mock("@conusai-kit/tenancy", () => ({
brandingSchema: { safeParse: (value: unknown) => ({ success: true, data: value ?? {} }) },
meGet: "me",
ORG_ROLES: ["viewer", "owner"],
orgGet: "org",
projectGet: "project",
projectList: "projects",
roleList: "roles",
workspaceList: "workspaces",
}))
vi.mock("@conusai-kit/tenancy/ui", () => ({
accessAuditPage: { load: vi.fn() },
builtInRoleName: (_i18n: unknown, role: string) => `kit:${role}`,
collectAccessEvents: vi.fn(),
invitationsPage: { load: mocks.invitationsLoad },
membersPage: { load: mocks.membersLoad },
requireSection: mocks.requireSection,
roleName: (_i18n: unknown, role: { name: string }) => `kit:${role.name}`,
rolePage: { load: vi.fn() },
rolesPage: { load: vi.fn() },
}))
vi.mock("@conusai-sdk/node-sdk", () => ({ ConusAIClient: class {} }))
const {
documentsData, // kit:documents
invitationsData,
membersData,
orgHomeData,
orgLayoutData,
requireAccessSection,
requireWorkspace,
} = await import("../apps/web/src/server/org-pages")
const i18n = { message: (key: string) => key, date: () => "2 Nov 2026", locale: "en" }
const page = (signedIn = true) => ({ i18n, caller: { call: mocks.call }, signedIn, path: "/acme" })
const refusal = (status: number): AppError =>
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
const thrown = (run: Promise<unknown>): Promise<unknown> =>
run.then(
() => {
throw new Error("expected a refusal")
},
(error: unknown) => error,
)
beforeEach(() => {
vi.clearAllMocks()
mocks.selected = new Set(["documents", "knowledge"])
mocks.pageContext.mockResolvedValue(page())
mocks.holds.mockResolvedValue(true)
mocks.requireSection.mockResolvedValue(undefined)
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
})
describe("the route files", () => {
const table = readFileSync(new URL("ROUTES.md", web), "utf8")
const files = readdirSync(routes)
it("have nothing left of the Next tree", () => {
expect(existsSync(new URL("app", web))).toBe(false)
expect(table).not.toContain("to port")
})
it("exist for every row of the table", () => {
const named = [...table.matchAll(/\| `([^`|]+\.tsx?)`[^|]*\| done/g)].map((row) => row[1] as string)
expect(named.length).toBeGreaterThan(30)
// `kit template:prune` removes these routes, and ROUTES.md stays the whole record of the port.
const pruned = new Set([
"auth.sign-up.tsx",
"$org.$workspace.$project.documents.index.tsx",
"$org.$workspace.$project.documents.$fileId.tsx",
])
expect(named.filter((file) => !files.includes(file) && !pruned.has(file))).toEqual([])
})
it.each(files.filter((file) => file !== "__root.tsx"))("%s keeps server code on the server", (file) => {
const source = readFileSync(new URL(file.replaceAll("$", "%24"), routes), "utf8")
// Only a type may be imported statically; the handlers import the file on the server.
expect(source).not.toMatch(/^import \{[^}]*\} from "\.\.\/server\//m)
expect(source).not.toMatch(/^import (?!type ).*"@app\/(config|modules)"$/m)
expect(source).not.toMatch(/\bexport const (dynamic|runtime)\b/)
expect(source).not.toMatch(/from "next/)
})
it("reach the browser with no root of the app's own packages", () => {
// Every file a route reaches by a static value import: what the browser's bundle starts from.
const seen = new Set<string>()
const walk = (url: URL): void => {
if (seen.has(url.href)) return
seen.add(url.href)
const source = readFileSync(url, "utf8")
for (const [, name] of source.matchAll(/^(?:import|export) (?!type )[^"]*"([^"]+)"/gm)) {
const label = `${url.pathname.split("/apps/web/")[1]} imports ${name}`
expect(label).not.toMatch(/imports @app\/(config|modules)$/)
expect(label).not.toMatch(/imports (\.\.?\/)+server\//)
if (!name?.startsWith(".") || name.includes("?")) continue
const target = [".ts", ".tsx"].map((ext) => new URL(`${name}${ext}`, url)).find((file) => existsSync(file))
if (target) walk(target)
}
}
for (const file of files.filter((name) => name !== "__root.tsx")) walk(new URL(file.replaceAll("$", "%24"), routes))
expect(seen.size).toBeGreaterThan(files.length)
})
it("leave the guard free of the page loaders", () => {
const guard = readFileSync(new URL("src/server/guard.ts", web), "utf8")
expect(guard).not.toMatch(/from "\.\/page"/)
})
})
describe("the organization route's loader", () => {
const organization = { name: "Acme", branding: { displayName: "Acme Co", logoLight: "/acme/brand/logo/light" } }
const answers = (procedure: unknown): unknown =>
procedure === "org"
? organization
: {
userId: "u1",
displayName: "Ada",
email: "ada@example.test",
organizations: [{ slug: "acme", name: "Acme" }],
workspaces: [],
}
it("sends a visitor to sign in", async () => {
mocks.pageContext.mockResolvedValue(page(false))
expect(isRedirect(await thrown(orgLayoutData({ org: "acme" })))).toBe(true)
expect(mocks.call).not.toHaveBeenCalled()
})
it("answers a stranger and a missing organization with one 404", async () => {
mocks.call.mockRejectedValue(refusal(404))
expect(isNotFound(await thrown(orgLayoutData({ org: "acme" })))).toBe(true)
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
})
it("hands the browser the navigation and nothing else of a manifest, with its labels translated", async () => {
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
const data = await orgLayoutData({ org: "acme" })
// The organization's own tab first, as on stage, whatever order the manifest gives.
expect(data.settings.sections).toEqual([
{ group: "organization", label: "shell.general", href: "/acme/settings", permission: "tenancy.org.manage" },
{ group: "organization", label: "tenancy.nav.members", href: "/acme/members" },
{ group: "organization", label: "app.audit", href: "/acme/audit", permission: "tenancy.audit.read" },
{ group: "personal", label: "account.link", href: "/acme/settings/account" },
])
expect(JSON.stringify(data)).not.toContain("a function that must not reach the browser")
})
it("shows an organization's own page in the workspace the person was last in, else the default one", async () => {
const workspaces = [
{ slug: "general", name: "General", isDefault: true },
{ slug: "research", name: "Research" },
]
mocks.call.mockImplementation(async (procedure: unknown) =>
procedure === "workspaces" ? { workspaces } : answers(procedure),
)
const lastIn = async (cookie?: string) => {
mocks.request.mockReturnValue(new Request("http://app.test/", cookie ? { headers: { cookie } } : {}))
return (await orgLayoutData({ org: "acme" })).workspace?.workspace.id
}
expect(await lastIn("app_workspace=acme%2Fresearch")).toBe("research")
expect(await lastIn()).toBe("general")
// Another organization's workspace, and one the person no longer sees, are not this organization's.
expect(await lastIn("app_workspace=northwind%2Fresearch")).toBe("general")
expect(await lastIn("app_workspace=acme%2Fgone")).toBe("general")
mocks.request.mockReturnValue(new Request("http://app.test/"))
})
it("puts the audit in Settings, and opens Settings on the first tab the person may open", async () => {
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
expect(await orgLayoutData({ org: "acme" })).toMatchObject({ settingsHref: "/acme/members" })
expect(await orgLayoutData({ org: "acme" })).not.toHaveProperty("nav")
})
it("offers a new workspace to whoever may make one, on a one-organization deployment too, and a new organization to whoever may found one", async () => {
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.workspace.manage"] })
mocks.pageContext.mockResolvedValue({ ...page(), mayCreateOrganization: true })
mocks.singleOrganization.mockReturnValue(true)
expect(await orgLayoutData({ org: "acme" })).toMatchObject({
workspaceCreateHref: "/acme/workspaces",
organizationCreateHref: "/onboarding",
})
mocks.permissionsIn.mockResolvedValue({ permissions: [] })
mocks.pageContext.mockResolvedValue({ ...page(), mayCreateOrganization: false })
const member = await orgLayoutData({ org: "acme" })
expect(member).not.toHaveProperty("workspaceCreateHref")
expect(member).not.toHaveProperty("organizationCreateHref")
mocks.pageContext.mockResolvedValue(page())
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
mocks.singleOrganization.mockReturnValue(false)
})
it("carries the organization's brand and the scope's choices", async () => {
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
const data = await orgLayoutData({ org: "acme" })
expect(data).toMatchObject({
name: "Acme Co",
style: { "--tenant": "1" },
logoLight: { src: "/acme/brand/logo/light?s=1x" },
organizations: [{ id: "acme", label: "Acme" }],
client: { org: "acme", user: "u1", locale: "en" },
})
})
})
describe("a page under an organization", () => {
it("is 404 for a workspace the person cannot see", async () => {
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
const { workspace, nav } = await requireWorkspace({ org: "acme", workspace: "general" })
expect(workspace).toEqual({ id: "general", label: "General" })
expect(nav.map((item) => item.href)).toEqual(["/acme/general"])
expect(isNotFound(await thrown(requireWorkspace({ org: "acme", workspace: "restricted" })))).toBe(true)
})
it("links the modules' pages of the workspace this person may open, and no other scope's", async () => {
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
mocks.nav.mockReturnValue([
{
scope: "workspace",
label: "reports.title",
href: "reports",
permission: "tenancy.audit.read",
module: "reports",
},
{
scope: "workspace",
label: "budgets.title",
href: "budgets",
permission: "budgets.budget.read",
module: "budgets",
},
{ scope: "project", label: "notes.title", href: "notes", module: "notes" },
])
const { nav } = await requireWorkspace({ org: "acme", workspace: "general" })
expect(nav.map((item) => item.href)).toEqual(["/acme/general", "/acme/general/modules/reports"])
mocks.nav.mockReturnValue([])
})
it("makes a page the module placed in Settings a tab there, and leaves it out of the sidebar", async () => {
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
mocks.nav.mockReturnValue([
{ scope: "workspace", label: "reports.title", href: "reports", module: "reports" },
{
scope: "workspace",
label: "reports.ai",
href: "reports/settings/ai",
placement: "settings",
module: "reports",
},
])
const { nav, settings } = await requireWorkspace({ org: "acme", workspace: "general" })
expect(nav.map((item) => item.href)).toEqual(["/acme/general", "/acme/general/modules/reports"])
expect(settings).toEqual([
{ group: "workspace", label: "reports.ai", href: "/acme/general/modules/reports/settings/ai" },
])
mocks.nav.mockReturnValue([])
})
it("is 404 in every loader at once, so whichever finishes first gives the same status", async () => {
mocks.call.mockRejectedValue(refusal(404))
expect(isNotFound(await thrown(orgHomeData({ org: "other" })))).toBe(true)
expect(isNotFound(await thrown(documentsData({ org: "other", workspace: "w", project: "p" })))).toBe(true) // kit:documents
})
// kit:documents:begin
it("refuses a default-off Documents page before any data call", async () => {
mocks.selected.clear()
expect(isNotFound(await thrown(documentsData({ org: "acme", workspace: "w", project: "p" })))).toBe(true)
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
expect(mocks.call).not.toHaveBeenCalled()
})
// kit:documents:end
// kit:documents:begin
it("carries the documents as the live list's seed, with the locale the browser writes their dates in", async () => {
const documents = [{ uploadedAt: "2026-11-02T00:00:00.000Z" }]
mocks.call.mockResolvedValue({ documents })
const data = await documentsData({ org: "acme", workspace: "general", project: "demo" })
expect(data.documents).toEqual(documents)
expect(data.locale).toBe(i18n.locale)
expect(data.accept).toEqual(["application/pdf"])
})
// kit:documents:end
})
describe("kit-tenancy's access pages", () => {
it("are 403 for a member without the section's permission", async () => {
mocks.requireSection.mockRejectedValue(refusal(403))
const error = (await thrown(requireAccessSection({ org: "acme", section: "members" }))) as { data?: unknown }
expect(error.data).toEqual({ forbidden: true })
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "403")
})
it("load at the level the path says", async () => {
mocks.membersLoad.mockResolvedValue({})
await membersData({ org: "acme" })
await membersData({ org: "acme", workspace: "w" })
await membersData({ org: "acme", workspace: "w", project: "p" })
expect(mocks.membersLoad.mock.calls.map((call) => call[1])).toEqual([
{ level: "org" },
{ level: "workspace", workspace: "w" },
{ level: "project", workspace: "w", project: "p" },
])
})
it("hand the invitations page its link path", async () => {
mocks.invitationsLoad.mockResolvedValue({})
await invitationsData({ org: "acme" })
expect(mocks.invitationsLoad.mock.calls[0]?.[1]).toEqual({ linkPath: "/invitations/acme/" })
})
})
+88
View File
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The server entry: what Next's proxy and instrumentation did. One nonce per request, named by the policy
// and given to the renderer; `x-path`; the status a loader asked for; a contract check before the first
// request; and a report for what escapes the handler.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
fetch: vi.fn<(request: Request, options?: unknown) => Promise<Response>>(),
loadAppEnv: vi.fn(),
loadAuthConfig: vi.fn(),
captureException: vi.fn(),
logError: vi.fn(),
}))
vi.mock("@tanstack/react-start/server-entry", () => ({
default: { fetch: mocks.fetch },
createServerEntry: <T>(entry: T): T => entry,
}))
vi.mock("@tanstack/react-start/server", () => ({ setResponseHeader: vi.fn(), setResponseStatus: vi.fn() }))
vi.mock("@app/config", () => ({
PROFILE: "postgres",
loadAppEnv: mocks.loadAppEnv,
currentObservability: () => ({ captureException: mocks.captureException, log: { error: mocks.logError } }),
}))
vi.mock("@conusai-kit/auth", () => ({ loadAuthConfig: mocks.loadAuthConfig }))
const { default: entry } = await import("../apps/web/src/server")
const { PAGE_STATUS_HEADER } = await import("../apps/web/src/server/guard")
const serve = (url = "http://app.test/acme/general/demo/notes?x=1"): Promise<Response> =>
(entry as unknown as { fetch(request: Request): Promise<Response> }).fetch(new Request(url))
beforeEach(() => {
mocks.fetch.mockReset()
mocks.fetch.mockImplementation(async () => new Response("<html></html>", { status: 200 }))
})
afterEach(() => vi.clearAllMocks())
describe("the server entry", () => {
it("gives the renderer the nonce the policy names, and tells a loader where the request is", async () => {
const response = await serve()
const request = mocks.fetch.mock.calls[0]?.[0] as Request
const nonce = request.headers.get("x-nonce") as string
expect(nonce).toBeTruthy()
expect(request.headers.get("x-path")).toBe("/acme/general/demo/notes?x=1")
expect(response.headers.get("content-security-policy")).toContain(`'nonce-${nonce}'`)
expect(mocks.fetch.mock.calls[0]?.[1]).toMatchObject({ context: { nonce } })
})
it("makes one nonce per request", async () => {
await serve()
await serve()
const [first, second] = mocks.fetch.mock.calls.map(([request]) => request.headers.get("x-nonce"))
expect(first).not.toBe(second)
})
it("applies the status a loader asked for and removes the header that carried it", async () => {
mocks.fetch.mockResolvedValueOnce(
new Response("<html>forbidden</html>", { status: 404, headers: { [PAGE_STATUS_HEADER]: "403" } }),
)
const response = await serve()
expect(response.status).toBe(403)
expect(response.headers.has(PAGE_STATUS_HEADER)).toBe(false)
expect(await response.text()).toContain("forbidden")
})
it("leaves a status nobody asked to change alone", async () => {
mocks.fetch.mockResolvedValueOnce(new Response(null, { status: 302, headers: { location: "/auth/sign-in" } }))
const response = await serve()
expect(response.status).toBe(302)
expect(response.headers.get("location")).toBe("/auth/sign-in")
})
it("parses the deployment contract once, before the first request is served", async () => {
await serve()
await serve()
// The entry's module state: checked by an earlier test's request at the latest, and never again.
expect(mocks.loadAppEnv.mock.calls.length).toBeLessThanOrEqual(1)
})
it("reports what escapes the handler, and still fails the request", async () => {
const fault = new Error("render failed")
mocks.fetch.mockRejectedValueOnce(fault)
await expect(serve()).rejects.toBe(fault)
expect(mocks.captureException).toHaveBeenCalledWith(fault, expect.objectContaining({ kind: "handler" }))
})
})
+122
View File
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Real signed assertions through the template's request adapter, across separate requests.
import { generateKeyPairSync, randomUUID, sign } from "node:crypto"
import { createServer } from "node:http"
import type { AddressInfo } from "node:net"
import { buildRegistry, Container } from "@conusai-kit/core"
import { afterEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({ runtime: vi.fn() }))
vi.mock("../apps/web/src/server/app", () => ({ webRuntime: mocks.runtime }))
vi.mock("@conusai-kit/tenancy", async (original) => ({
...(await original<Record<string, unknown>>()),
createTenancyHooks: () => ({}),
}))
const { supportOboCaller } = await import("../apps/web/src/server/support-obo")
const issuer = "https://platform.example.test"
const audience = "https://app.example.test"
const keys = generateKeyPairSync("ed25519")
const jwk = { ...keys.publicKey.export({ format: "jwk" }), kid: "synthetic", alg: "EdDSA", use: "sig" }
const registry = buildRegistry([])
const httpFetch = globalThis.fetch
function configure() {
mocks.runtime.mockReturnValue({
data: {},
registry,
env: {
deployment: {
CONUSAI_API_URL: `${issuer}/api`,
CONUSAI_PUBLIC_URL: audience,
CONUSAI_ORG_ID: "3",
CONUSAI_PROJECT_ID: "4",
CONUSAI_ENVIRONMENT_ID: "5",
},
},
})
vi.stubGlobal(
"fetch",
vi.fn(async () => Response.json({ keys: [jwk] })),
)
}
function assertion(extra: Record<string, unknown> = {}) {
const now = Math.floor(Date.now() / 1000)
const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString("base64url")
const body = `${encode({ alg: "EdDSA", typ: "conusai-obo+jwt", kid: "synthetic" })}.${encode({
iss: issuer,
aud: audience,
sub: "synthetic-user",
sub_ns: "app",
app_tenant: "tenant-a",
conv: "synthetic-conversation",
channel: "sc_synthetic",
org: "3",
project: "4",
env: "5",
jti: randomUUID(),
iat: now,
exp: now + 60,
...extra,
})}`
return `${body}.${sign(null, Buffer.from(body), keys.privateKey).toString("base64url")}`
}
function request(token?: string, pathname = "/api/mcp") {
return new Request(`${audience}${pathname}`, {
headers: token ? { "Conusai-On-Behalf-Of": token } : {},
})
}
afterEach(() => {
vi.unstubAllGlobals()
vi.clearAllMocks()
})
describe("the Support OBO request adapter", () => {
it("accepts a signed assertion once and refuses its replay on a separate request", async () => {
configure()
const token = assertion()
expect(await supportOboCaller(request(token), new Container())).not.toBeNull()
await expect(supportOboCaller(request(token), new Container())).rejects.toMatchObject({ status: 401 })
})
it("refuses another environment and permits a new assertion", async () => {
configure()
await expect(supportOboCaller(request(assertion({ env: "6" })), new Container())).rejects.toMatchObject({
status: 401,
})
expect(await supportOboCaller(request(assertion()), new Container())).not.toBeNull()
})
it("keeps the replay boundary across real HTTP requests", async () => {
configure()
const server = createServer(async (incoming, response) => {
try {
const token = incoming.headers["conusai-on-behalf-of"]
const caller = await supportOboCaller(request(typeof token === "string" ? token : undefined), new Container())
response.writeHead(caller ? 200 : 204).end()
} catch (error) {
response.writeHead((error as { status?: number }).status ?? 500).end()
}
})
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
const origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
const token = assertion()
try {
const headers = { "Conusai-On-Behalf-Of": token }
expect((await httpFetch(`${origin}/api/mcp`, { headers })).status).toBe(200)
expect((await httpFetch(`${origin}/api/mcp`, { headers })).status).toBe(401)
expect((await httpFetch(`${origin}/api/mcp`)).status).toBe(204)
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
}
})
it("leaves ordinary requests and non-MCP routes on session authentication", async () => {
expect(await supportOboCaller(request(), new Container())).toBeNull()
expect(await supportOboCaller(request("ignored", "/api/ready"), new Container())).toBeNull()
expect(mocks.runtime).not.toHaveBeenCalled()
})
})
+193
View File
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Actual app caller and native readiness protocol over a synthetic transport; no live service proof.
import { loadAppEnv, platformClients } from "@app/config"
import { type Actor, buildRegistry, Container, createCaller } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
import { SUPPORT_PLATFORM, SUPPORT_PROCESSING_STATUS, supportModule } from "@conusai-kit/support"
import { createTenancyHooks, orgMemberships, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
import { afterEach, expect, it, vi } from "vitest"
import { bindWebPorts, nativeHelpSelectors } from "../apps/web/src/server/ports.js"
import { businessServiceComposition } from "../config/src/business-services.js"
const selection = vi.hoisted(() => ({ documents: false }))
vi.mock("@app/modules", async (original) => ({
...(await original<Record<string, unknown>>()),
moduleSelected: (id: string) =>
id === "documents" ? selection.documents : ["agent", "guides", "support"].includes(id),
}))
afterEach(() => {
selection.documents = false
vi.restoreAllMocks()
})
const descriptor = `sha256:${"a".repeat(64)}`
const source = "b".repeat(40)
const deployment = {
CONUSAI_API_URL: "https://platform.example.test/api",
CONUSAI_API_TOKEN: "synthetic-token",
CONUSAI_ORG_ID: "1",
CONUSAI_PROJECT_ID: "9",
CONUSAI_ENVIRONMENT_ID: "3",
CONUSAI_ORG_SLUG: "org",
CONUSAI_WORKSPACE_SLUG: "workspace",
CONUSAI_PROJECT_SLUG: "project",
CONUSAI_PLATFORM_RELEASE_DESCRIPTOR: descriptor,
CONUSAI_PLATFORM_SOURCE_COMMIT: source,
}
const observation = () => ({
component: {
id: "support",
observed_at: new Date().toISOString(),
facts: {
installed: "true",
enabled: "true",
entitled: "true",
configured: "true",
ready: "true",
},
},
authorized: "true",
environment_bound: "true",
compatible: "true",
dependencies_ready: "true",
policy_epoch: 2,
descriptor_digest: descriptor,
source_commit: source,
organization_id: 1,
project_id: 9,
environment_id: 3,
app_scope: "org",
authorization_checked_at: new Date().toISOString(),
})
function composition() {
const modules = [tenancyModule, supportModule]
const registry = buildRegistry(modules)
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const base = new Container()
base.value(DATA_SOURCE, data)
base.value(SYSTEM_DATA, data)
const env = loadAppEnv("web", {
...deployment,
NODE_ENV: "test",
POSTGRES_URL: "postgresql://app_runtime:fixture@db/app",
POSTGRES_READER_URL: "postgresql://app_reader:fixture@db/app",
})
bindWebPorts(base, platformClients(env, registry), registry)
const root = base.child()
root.value(SUPPORT_PLATFORM, { mode: "platform", channel: "spk_actual", client: {} } as never)
const hooks = businessServiceComposition({ deployment }, modules, "postgres", nativeHelpSelectors)
const caller = (actor: Actor) => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, ...hooks }, { actor })
}
return { data, root, caller }
}
it("returns empty without touching Documents when that module is not selected", async () => {
const app = composition()
const scope = {
level: "project" as const,
orgId: "local-org",
workspaceId: "local-workspace",
projectId: "local-project",
actor: { userId: "member" },
}
const read = vi.spyOn(app.data, "forScope")
expect(await app.root.resolve(SUPPORT_PROCESSING_STATUS).read(scope)).toEqual({
state: "empty",
processing: 0,
extracted: 0,
failed: 0,
})
expect(read).not.toHaveBeenCalled()
})
// kit:documents:begin
it("reads the requested project's processing counts when Documents is selected", async () => {
selection.documents = true
const app = composition()
const scope = {
level: "project" as const,
orgId: "local-org",
workspaceId: "local-workspace",
projectId: "local-project",
actor: { userId: "member" },
}
const list = vi.fn(async () => ({ items: [{ status: "processing" }, { status: "extracted" }, { status: "failed" }] }))
const read = vi.spyOn(app.data, "forScope").mockReturnValue({ of: () => ({ list }) } as never)
expect(await app.root.resolve(SUPPORT_PROCESSING_STATUS).read(scope)).toEqual({
state: "failed",
processing: 1,
extracted: 1,
failed: 1,
})
expect(read).toHaveBeenCalledWith(scope)
})
// kit:documents:end
it("admits the containing org service after actual project authorization and preserves native denials", async () => {
const app = composition()
const user = await app.data.global(users).insert({
issuer: "https://identity.example.test",
subject: "owner",
email: "owner@example.test",
displayName: "Owner",
locale: "en",
})
const actor = { userId: user.id }
const caller = app.caller(actor)
const org = (await caller.callByName("tenancy.org.create", { slug: "alpha", name: "Alpha" })) as { orgId: string }
await caller.callByName("tenancy.project.create", {
org: "alpha",
workspace: "general",
slug: "main",
name: "Main",
moduleKey: "support",
})
const input = { org: "alpha", workspace: "general", project: "main" }
let current = observation()
let status = 200
const fetch = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async () => (status === 200 ? Response.json(current) : new Response(null, { status })))
const dispatch = (query = input) => app.caller(actor).callByName("support.projectProcessingStatus.get", query)
await expect(dispatch({ ...input, project: "missing" })).rejects.toMatchObject({ status: 404 })
expect(fetch).not.toHaveBeenCalled()
await expect(dispatch()).resolves.toEqual({ state: "empty", processing: 0, extracted: 0, failed: 0 })
const first = fetch.mock.calls[0]
if (!first) throw new Error("Expected native readiness request")
const [url, request] = first
expect(String(url)).toBe(
"https://platform.example.test/api/v1/orgs/org/workspaces/workspace/projects/project/environments/3/platform-services/support/readiness",
)
expect(request?.redirect).toBe("error")
expect(request?.headers).toMatchObject({ authorization: "Bearer synthetic-token" })
expect(JSON.parse(String(request?.body))).toEqual({
api_range: "=1.0.0-preview.20261103",
scopes: ["support:widget-sessions"],
app_scope: "org",
channel: "spk_actual",
})
for (const changed of [{ project_id: 10 }, { app_scope: "project" }, { environment_id: 4 }]) {
current = { ...observation(), ...changed }
await expect(dispatch()).rejects.toMatchObject({ code: "service_permission_missing" })
}
current = { ...observation(), source_commit: "c".repeat(40) }
await expect(dispatch()).rejects.toMatchObject({ code: "service_incompatible" })
status = 403
await expect(dispatch()).rejects.toMatchObject({ code: "service_permission_missing" })
const beforeRevocation = fetch.mock.calls.length
await app.data.unitOfWork.run({ level: "org", orgId: org.orgId, actor }, async (tx) => {
const membership = (await tx.of(orgMemberships).list({ where: [{ field: "userId", op: "eq", value: user.id }] }))
.items[0]
if (!membership) throw new Error("Expected owner membership")
await tx.of(orgMemberships).delete(membership.id)
})
await expect(dispatch()).rejects.toMatchObject({ status: 404 })
expect(fetch).toHaveBeenCalledTimes(beforeRevocation)
})
+284
View File
@@ -0,0 +1,284 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The whole app of this template, wired the way apps/web and apps/worker wire it, on the in-memory
// data source and the fakes of fakes.ts: the registry of every module, one caller per actor, the
// outbox relay's behaviour and the worker's job runner.
//
// `deliver()` stands in for the outbox relay (kit-jobs-pgboss): it drains the outbox once, runs each
// record's subscribers in a child container with the record's organization and a system actor, and
// marks it published -- exactly once per subscriber, never more forgiving than the real relay.
// `runJobs()` stands in for the worker: it runs every queued job in a child container carrying the
// scope the job was enqueued in, with a system actor. What neither does is retry, so a test of a
// retry runs the handler again itself.
import {
DOCUMENT_EXCLUDED_PAGE_CLASSES,
DOCUMENT_MODEL,
DOCUMENT_OCR,
DOCUMENTS_SCOPE,
documentsModule,
MODEL_GATEWAY,
type PageClass,
} from "@app/modules/documents"
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { FILES_AUDIT, FILES_REGISTRY, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { JOB_ON_BEHALF_OF, JOB_SCOPE } from "@conusai-kit/jobs-pgboss"
import { NOTIFICATIONS_REPOS } from "@conusai-kit/notifications"
import { createModelGateway } from "@conusai-kit/platform"
import { PROPOSALS_AUDIT, PROPOSALS_REGISTRY, PROPOSALS_SCOPE } from "@conusai-kit/proposals"
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import {
fakeJobQueue,
fakeObjectStore,
recordingAudit,
recordingUsage,
uploadViaProcedures,
} from "@conusai-kit/testing"
import type { FakeChatResponder } from "@conusai-kit/testing/fake-platform"
import { bindNativeJobAdmission } from "../../apps/worker/src/native-admission.js"
import { appModules } from "../../modules/index.js"
import { deterministicModel } from "./fake-model.js"
import { syntheticNativeReadiness } from "./native-readiness.js"
import { type FakePlatform, startOcr } from "./ocr.js"
const started: FakePlatform[] = []
/** Stops every fake platform the test file started: call it from `afterEach`. */
export async function closeApps(): Promise<void> {
await Promise.all(started.splice(0).map((platform) => platform.close()))
}
export interface SeededProject {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly workspaceSlug: string
readonly projectSlug: string
readonly orgId: string
readonly workspaceId: string
readonly projectId: string
/** The three slugs a project-scoped procedure's input carries. */
readonly slugs: { org: string; workspace: string; project: string }
}
/** `document` is what the fake platform's OCR reads every file as: a `conusai.ocr.document/v1`. */
export async function makeDocumentsApp(options: {
document: Record<string, unknown>
/** What the model answers; by default the deterministic reader of fake-model.ts. */
responder?: FakeChatResponder
/** The page classes the deployment leaves out; by default the module's own (drawings and empty pages). */
nativeAdmission?: boolean
excludedPageClasses?: readonly PageClass[]
}) {
const registry = buildRegistry([...appModules.filter((entry) => entry.id !== "documents"), documentsModule])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const store = fakeObjectStore()
const queue = fakeJobQueue()
const audit = recordingAudit()
const usage = recordingUsage()
const { platform, ocr } = await startOcr(options.document)
started.push(platform)
const root = new Container()
root.value(DATA_SOURCE, data)
bindNativeJobAdmission(root, registry, options.nativeAdmission === false ? undefined : syntheticNativeReadiness)
root.value(SYSTEM_DATA, data)
root.value(OBJECT_STORE, store)
root.value(JOB_QUEUE, queue)
root.value(FILES_REGISTRY, registry)
root.value(FILES_AUDIT, audit)
root.value(FILES_USAGE, usage)
root.value(PROPOSALS_REGISTRY, registry)
root.value(PROPOSALS_AUDIT, audit)
root.value(DOCUMENT_OCR, ocr)
// The model is the real gateway client over HTTP to the fake platform, whose reply is scripted.
const modelRequests: Array<Record<string, unknown>> = []
const script = (responder: FakeChatResponder | undefined): void =>
platform.ai.respond(
responder &&
((request) => {
modelRequests.push(request)
return responder(request)
}),
)
script(options.responder ?? deterministicModel)
root.value(
MODEL_GATEWAY,
createModelGateway({ CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "deployment-token" }),
)
root.value(DOCUMENT_MODEL, "fake-model")
if (options.excludedPageClasses) root.value(DOCUMENT_EXCLUDED_PAGE_CLASSES, options.excludedPageClasses)
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, serviceReadiness: syntheticNativeReadiness }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
const delivered: Array<{ name: string }> = []
const deliver = async (): Promise<number> => {
const pending = await data.outbox.pending(500)
let ran = 0
for (const record of pending) {
const entry = registry.events[record.name]
if (entry) {
for (const subscriber of entry.subscribers) {
const container = root.child()
const scope = { level: "org" as const, actor: systemActor("app.relay"), orgId: record.orgId }
container.value(FILES_SCOPE, scope)
container.value(PROPOSALS_SCOPE, scope)
container.value(NOTIFICATIONS_REPOS, data.forScope(scope))
await subscriber.handler(
{ name: record.name, payload: record.payload },
{ container, correlationId: record.id },
)
delivered.push({ name: record.name })
ran += 1
}
}
await data.outbox.markPublished([record.id])
}
return ran
}
/** The jobs not yet run, in the order they were enqueued. */
let consumed = 0
const runJobs = async (options: { finalAttempt?: boolean; signal?: AbortSignal } = {}): Promise<number> => {
let ran = 0
while (consumed < queue.jobs.length) {
const job = queue.jobs[consumed] as (typeof queue.jobs)[number]
consumed += 1
const registered = registry.jobs[job.job]
if (!registered) throw new Error(`no module declares the job ${job.job}`)
const container = root.child()
const tenancy = createTenancyHooks({ data, registry })
container.value(TENANCY, tenancy)
const trusted = { ...job.scope, actor: systemActor(`${job.job}`) }
container.value(DOCUMENTS_SCOPE, trusted)
container.value(JOB_SCOPE, trusted)
container.value(JOB_ON_BEHALF_OF, null)
await registered.def.handler(registered.def.payload.parse(job.payload), {
container,
correlationId: job.id,
attempt: 1,
signal: options.signal ?? new AbortController().signal,
...(options.finalAttempt === undefined ? {} : { finalAttempt: options.finalAttempt }),
})
ran += 1
}
return ran
}
async function seedProject(slug: string, email: string): Promise<SeededProject> {
const owner = await signUp(email)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "documents",
})) as { projectId: string; slug: string }
return {
owner,
call,
orgSlug: org.slug,
workspaceSlug: workspace.slug,
projectSlug: project.slug,
orgId: org.orgId,
workspaceId: workspace.workspaceId,
projectId: project.projectId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A signed-up user who joined the project's organization with a role. */
async function addMember(project: SeededProject, email: string, role: "viewer" | "member" | "admin") {
const actor = await signUp(email)
const invite = (await project.call.callByName("tenancy.member.invite", {
org: project.orgSlug,
email,
role,
})) as { token: string }
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: project.orgSlug, token: invite.token })
return { actor, call }
}
return {
registry,
data,
entities,
store,
queue,
audit,
usage,
/** The OCR port the worker uses, observed, and the fake platform behind it. */
ocr,
platform,
/** Every request the model was sent, as the gateway received it, and a way to change its answers. */
model: { requests: modelRequests, respond: script },
caller,
signUp,
deliver,
runJobs,
delivered,
seedProject,
addMember,
}
}
export type DocumentsApp = Awaited<ReturnType<typeof makeDocumentsApp>>
/** Some text as the bytes of a file, for an upload. */
export const bytesOf = (text: string): Uint8Array => new TextEncoder().encode(text)
/** Uploads a file (a PDF unless told otherwise) as its project's owner and carries it through the relay and the worker. */
export async function uploadAndProcess(
app: DocumentsApp,
project: SeededProject,
filename = "report.pdf",
contentType = "application/pdf",
) {
const file = await uploadViaProcedures(project.call, project.slugs, {
filename,
contentType,
// The files module checks the first bytes against the type the upload declares, so a picture
// starts with the PNG signature and a document with the PDF one.
bytes:
contentType === "image/png"
? new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ...bytesOf(`fixture ${filename}`)])
: bytesOf(`%PDF-1.4 fixture ${filename}`),
})
await app.deliver()
await app.runJobs()
return file
}
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The test double of the AI gateway for the documents module: a deterministic reader, standing in for a
// model behind `platform.ai.respond` of the fake platform.
//
// It answers the extraction request exactly as a well-behaved model would: it finds the page's text
// between the tags the prompt names, takes every `label: value` line as a field, and quotes the line
// itself. It never sees an image, never invents a word, and returns the same lines for the same text,
// so a test (or the offline eval) that runs it measures the module's own code -- the request it builds,
// the schema it parses, the verification, the cache, the proposals -- and not a model.
import { PAGE_TEXT_CLOSE, PAGE_TEXT_OPEN } from "@app/modules/documents"
import type { FakeChatReply } from "@conusai-kit/testing/fake-platform"
const FIELD = /^\s*([^::]+?)\s*[::]\s*(\S.*?)\s*$/u
const MAX_LABEL_CHARS = 60
const MAX_LABEL_WORDS = 6
const MAX_LINE_CHARS = 2_000
export interface ReadLineOfModel {
label: string
value: string
quote: string
}
/**
* The fields a reader of two line shapes finds in a page's text: `label: value`, and a table row
* `first cell | other cells`, whose label is the first cell and whose value is the rest, as the prompt
* asks a model to read it. The first row of a block of rows is the table's header, which names the
* columns and is not a field.
*/
export function keyValueLines(text: string): ReadLineOfModel[] {
const found: ReadLineOfModel[] = []
let inTable = false
for (const line of text.split("\n")) {
if (line.includes(" | ")) {
const header = !inTable
inTable = true
const [label, ...rest] = line.split(" | ").map((cell) => cell.trim())
if (!header && label && rest.length > 0 && rest.every((cell) => cell !== "") && line.length <= MAX_LINE_CHARS) {
found.push({ label, value: rest.join(" | "), quote: line.trim() })
}
continue
}
inTable = false
const match = FIELD.exec(line)
if (!match) continue
const label = match[1] as string
const value = match[2] as string
if (label.length > MAX_LABEL_CHARS || label.split(/\s+/u).length > MAX_LABEL_WORDS) continue
if (line.length > MAX_LINE_CHARS) continue
found.push({ label, value, quote: line.trim() })
}
return found
}
type Part = { type?: string; text?: string }
/** The page text of an extraction request: what lies between the prompt's tags in the user's message. */
export function pageTextOf(request: Record<string, unknown>): string | undefined {
const messages = (request.messages ?? []) as Array<{ role?: string; content?: string | Part[] }>
const user = [...messages].reverse().find((message) => message.role === "user")
const content = user?.content
const text = typeof content === "string" ? content : (content ?? []).map((part) => part.text ?? "").join("\n")
const from = text.indexOf(PAGE_TEXT_OPEN)
const to = text.lastIndexOf(PAGE_TEXT_CLOSE)
return from < 0 || to < from ? undefined : text.slice(from + PAGE_TEXT_OPEN.length + 1, to - 1)
}
/** What an extraction request is answered with by the deterministic reader. */
export const deterministicModel = (request: Record<string, unknown>): FakeChatReply => {
const text = pageTextOf(request)
return { content: JSON.stringify({ lines: text === undefined ? [] : keyValueLines(text) }) }
}
+114
View File
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Real images for the logo tests: a PNG written by hand, so a test can break one chunk, and a WebP
// that libwebp wrote. The kit's logo checks read structure and decode pixels, so a few magic bytes
// no longer stand in for an image.
import { deflateSync, inflateSync } from "node:zlib"
const TABLE = (() => {
const table = new Uint32Array(256)
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
table[n] = c >>> 0
}
return table
})()
const crc32 = (bytes: Uint8Array): number => {
let c = 0xffffffff
for (const byte of bytes) c = (TABLE[(c ^ byte) & 0xff] as number) ^ (c >>> 8)
return (c ^ 0xffffffff) >>> 0
}
const concat = (...parts: Uint8Array[]): Buffer => Buffer.concat(parts)
export const chunk = (type: string, data: Uint8Array): Buffer => {
const out = Buffer.alloc(12 + data.length)
out.writeUInt32BE(data.length, 0)
out.write(type, 4, "latin1")
out.set(data, 8)
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length)
return out
}
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
/** A PNG over `pixel(x, y)` -> [r, g, b, a]. */
export function pngOf(width: number, height: number, pixel: (x: number, y: number) => readonly number[]): Buffer {
const header = Buffer.alloc(13)
header.writeUInt32BE(width, 0)
header.writeUInt32BE(height, 4)
header.set([8, 6, 0, 0, 0], 8)
const rows = Buffer.alloc(height * (1 + width * 4))
for (let y = 0; y < height; y++)
for (let x = 0; x < width; x++) rows.set(pixel(x, y), y * (1 + width * 4) + 1 + x * 4)
return concat(SIGNATURE, chunk("IHDR", header), chunk("IDAT", deflateSync(rows)), chunk("IEND", new Uint8Array()))
}
/** A teal disc on transparent that fills the height. */
export const markPng = (width: number, height: number, colour: readonly number[] = [0x80, 0xcd, 0xc6, 255]): Buffer =>
pngOf(width, height, (x, y) => {
const dx = x - width / 2
const dy = y - height / 2
return dx * dx + dy * dy < (Math.min(width, height) / 2) ** 2 ? colour : [0, 0, 0, 0]
})
/** Pseudo-random pixels, which PNG cannot compress: a file about as large as its pixels. */
export function noisePng(width: number, height: number): Buffer {
let seed = 12345
const next = () => {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
return seed >>> 24
}
return pngOf(width, height, () => [next(), next(), next(), 255])
}
/** A lossless WebP of a teal disc, 400 x 100, written by libwebp. */
export const MARK_WEBP = Buffer.from(
"UklGRvwAAABXRUJQVlA4TO8AAAAvj8EYEA8wzQM4xvMf8HBU27bT0DNEwpeCNJCGlC/hD9NvKuXe91InEf135EaSInlgeU8DlfmEzM4yACXbFuj4j/SHeNfSX9Nfnu/A/oT+QQToH6RHgfxN+hfPj8CEJzn1l/Qfjs821P+i/1V6Fqj/Rf+r47OefxgAlmFCTjxw6IfOOGVjV5zS2pWmBLu2Uzq7jlN6u85T8NphUv7KDdPK/6D3cdV7G3chH3dsH5OHjwnKxyToY6JVzNmKWd5FwlDkHkW2UuQ3RUZU5FBF1lXkaUFmV/QCiu5B0W8oOhRBT6PoghR9k6LTEvRmdrq5DAA=",
"base64",
)
export const dimensions = (png: Uint8Array): { width: number; height: number } => ({
width: Buffer.from(png).readUInt32BE(16),
height: Buffer.from(png).readUInt32BE(20),
})
/** The [r, g, b, a] of one pixel of an 8-bit RGBA, non-interlaced PNG (what the renderers write), by unfiltering its rows. */
export function pixelAt(png: Uint8Array, x: number, y: number): number[] {
const bytes = Buffer.from(png)
const { width, height } = dimensions(bytes)
if (bytes[24] !== 8 || bytes[25] !== 6 || bytes[28] !== 0)
throw new Error("pixelAt reads 8-bit RGBA PNGs without interlace")
const data: Buffer[] = []
for (let at = 8; at < bytes.length; ) {
const length = bytes.readUInt32BE(at)
if (bytes.toString("latin1", at + 4, at + 8) === "IDAT") data.push(bytes.subarray(at + 8, at + 8 + length))
at += 12 + length
}
const raw = inflateSync(Buffer.concat(data))
const stride = width * 4
const rows: Buffer[] = []
for (let row = 0; row < height; row++) {
const filter = raw[row * (stride + 1)] as number
const line = Buffer.from(raw.subarray(row * (stride + 1) + 1, (row + 1) * (stride + 1)))
const above = rows[row - 1]
for (let i = 0; i < stride; i++) {
const left = i >= 4 ? (line[i - 4] as number) : 0
const up = above ? (above[i] as number) : 0
const corner = above && i >= 4 ? (above[i - 4] as number) : 0
const predictor = [0, left, up, (left + up) >> 1, paeth(left, up, corner)][filter] as number
line[i] = ((line[i] as number) + predictor) & 0xff
}
rows.push(line)
}
const line = rows[y] as Buffer
return [line[x * 4], line[x * 4 + 1], line[x * 4 + 2], line[x * 4 + 3]] as number[]
}
function paeth(a: number, b: number, c: number): number {
const p = a + b - c
const [pa, pb, pc] = [Math.abs(p - a), Math.abs(p - b), Math.abs(p - c)]
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Which `modules/<id>/__tests__/isolation.ts` files the tenant-isolation suite reads: those of the modules
// the app registers. A module folder that is present but not registered (built and tested, held until its
// owner opens it) serves no procedure, so its fixtures would name procedures this registry does not have.
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
export function isolationFilesOf(modulesDir: string, registered: Iterable<string>): string[] {
const ids = new Set(registered)
return readdirSync(modulesDir)
.filter((id) => ids.has(id))
.map((id) => join(modulesDir, id, "__tests__", "isolation.ts"))
.filter((path) => existsSync(path))
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Explicit synthetic eligibility for local fake-provider fixtures only. No crypto/live proof.
import type { ServiceReadiness } from "@conusai-kit/core"
import type { OptionalModuleId } from "../../modules/inclusion.js"
export const syntheticNativeReadiness: ServiceReadiness = {
profile: "postgres",
async observe() {
return {
desiredGeneration: 1,
observedGeneration: 1,
observedAt: Date.now(),
installed: true,
enabled: true,
entitled: true,
authorized: true,
configured: true,
compatible: true,
dependenciesReady: true,
partitionBound: "unknown",
ready: true,
nativeAuthority: {
kind: "verified_release_set_v1",
policyEpoch: 1,
expectedDescriptorDigest: `sha256:${"a".repeat(64)}`,
observedDescriptorDigest: `sha256:${"a".repeat(64)}`,
expectedSourceCommit: "b".repeat(40),
observedSourceCommit: "b".repeat(40),
authorityCheckedAt: Date.now(),
},
}
},
}
/**
* The optional modules whose source this app holds, as `modules/index.ts` composes them. `kit template:prune
* --without` removes a line with its module, so a journey that asks for a pruned module runs without it.
*/
const WITH_SOURCE: readonly OptionalModuleId[] = [
"activities",
"archive",
"catalog",
"crm",
"inventory",
"invoicing",
"ledger",
"documents", // kit:documents
"knowledge", // kit:knowledge
"agent", // kit:agent
"guides", // kit:guides
"support", // kit:support
]
/** A journey opts in only to the modules it exercises, of those this app has; unknown IDs remain off. */
export function selectedModules(requested: readonly OptionalModuleId[]) {
const selected = requested.filter((id) => WITH_SOURCE.includes(id))
const record = { schemaVersion: 1 as const, selected, everAdopted: selected }
return {
moduleInclusion: record,
moduleSelected: (id: OptionalModuleId) => selected.includes(id),
moduleAdopted: (id: OptionalModuleId) => record.everAdopted.includes(id),
}
}
+195
View File
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The whole app of this template on the in-memory data source, wired the way apps/web and apps/worker
// wire it, for the notes module's tests: the registry of every module, one caller per actor, the
// outbox relay's behaviour and the worker's job runner.
//
// `deliver()` stands in for the outbox relay: it drains the outbox once, runs each record's
// subscribers in a child container with the record's organization and a system actor, and marks it
// published -- exactly once per subscriber, never more forgiving than the real relay. `runJobs()`
// stands in for the worker's job runner. What neither does is retry, so a test of a redelivery
// runs a handler again itself.
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import {
createMemoryDataSource,
DATA_SOURCE,
entitiesOf,
JOB_QUEUE,
OBJECT_STORE,
SYSTEM_DATA,
} from "@conusai-kit/data"
import { FILES_AUDIT, FILES_REGISTRY, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import { NOTIFICATIONS_REPOS } from "@conusai-kit/notifications"
import { PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
import {
fakeJobQueue,
fakeObjectStore,
recordingAudit,
recordingUsage,
uploadViaProcedures,
} from "@conusai-kit/testing"
import { appModules } from "../../modules/index.js"
import { NOTES_FLAGS, type NotesFlags } from "../../modules/notes/index.js"
export interface SeededProject {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly orgId: string
readonly slugs: { org: string; workspace: string; project: string }
}
/** Standard base64 of some text, for the upload procedure's body. */
export const bytesOf = (text: string): Uint8Array => new TextEncoder().encode(text)
export function makeNotesApp(options: { flags?: NotesFlags } = {}) {
const registry = buildRegistry([...appModules])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const store = fakeObjectStore()
const queue = fakeJobQueue()
const audit = recordingAudit()
const usage = recordingUsage()
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(OBJECT_STORE, store)
root.value(JOB_QUEUE, queue)
root.value(FILES_REGISTRY, registry)
root.value(FILES_AUDIT, audit)
root.value(FILES_USAGE, usage)
root.value(PROPOSALS_REGISTRY, registry)
if (options.flags) root.value(NOTES_FLAGS, options.flags)
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
/** Runs every subscriber of every record in the outbox, once each; answers how many ran. */
const deliver = async (): Promise<number> => {
let ran = 0
for (const record of await data.outbox.pending(500)) {
const entry = registry.events[record.name]
for (const subscriber of entry?.subscribers ?? []) {
const container = root.child()
container.value(FILES_SCOPE, { level: "org", actor: systemActor("app.relay"), orgId: record.orgId })
container.value(NOTIFICATIONS_REPOS, data.forScope(container.resolve(FILES_SCOPE)))
await subscriber.handler(
{ name: record.name, payload: record.payload },
{ container, correlationId: record.id },
)
ran += 1
}
await data.outbox.markPublished([record.id])
}
return ran
}
/** The jobs not yet run, in the order they were enqueued. */
let consumed = 0
const runJobs = async (): Promise<number> => {
let ran = 0
while (consumed < queue.jobs.length) {
const job = queue.jobs[consumed] as (typeof queue.jobs)[number]
consumed += 1
const registered = registry.jobs[job.job]
if (!registered) throw new Error(`no module declares the job ${job.job}`)
const container = root.child()
container.value(TENANCY, createTenancyHooks({ data, registry }))
container.value(FILES_SCOPE, { ...job.scope, actor: systemActor(job.job) })
await registered.def.handler(registered.def.payload.parse(job.payload), {
container,
correlationId: job.id,
attempt: 1,
})
ran += 1
}
return ran
}
async function seedProject(slug: string, email: string): Promise<SeededProject> {
const owner = await signUp(email)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "notes",
})) as { slug: string }
return {
owner,
call,
orgSlug: org.slug,
orgId: org.orgId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A signed-up user who joined the project's organization with a role. */
async function addMember(project: SeededProject, email: string, role: "viewer" | "member" | "admin") {
const actor = await signUp(email)
const invite = (await project.call.callByName("tenancy.member.invite", {
org: project.orgSlug,
email,
role,
})) as { token: string }
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: project.orgSlug, token: invite.token })
return { actor, call }
}
/** Uploads a small text file through the files module, the way the page's form does. */
async function upload(project: SeededProject, filename = "minutes.txt", text = "agreed") {
return await uploadViaProcedures(project.call, project.slugs, {
filename: filename,
contentType: "text/plain",
bytes: bytesOf(text),
})
}
return {
registry,
/** The process container the procedures resolve their ports from (a seed script needs it). */
root,
data,
entities,
store,
queue,
audit,
usage,
caller,
signUp,
deliver,
runJobs,
seedProject,
addMember,
upload,
}
}
export type NotesApp = ReturnType<typeof makeNotesApp>
+61
View File
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The OCR path of a test: the real `@conusai-sdk/ocr` client (through kit-platform's `DocumentReader`)
// over HTTP to the fake platform of @conusai-kit/testing, which accepts a job and answers it as the
// platform's job API does. Nothing here stands in for the OCR path; `observed` only watches it, and can
// make one call fail the way a network does, which the fake platform has no way to do.
import { type DocumentOcr, sdkDocumentOcr } from "@app/modules/documents"
import { createDocumentReader } from "@conusai-kit/platform"
import { startFakePlatform } from "@conusai-kit/testing/fake-platform"
export type FakePlatform = Awaited<ReturnType<typeof startFakePlatform>>
export interface ObservedOcr extends DocumentOcr {
/** Every document accepted, with the idempotency key it came with. */
readonly submitted: Array<{ jobId: number; filename: string; idempotencyKey: string }>
/** Every `wait`, by job id: a retried job must resume its job and not submit another. */
readonly waited: number[]
/** The next `wait` throws this, once, before asking the platform anything. */
failWaitOnce(error: Error): void
}
export function observed(inner: DocumentOcr): ObservedOcr {
const submitted: ObservedOcr["submitted"] = []
const waited: number[] = []
let nextWait: Error | undefined
return {
submitted,
waited,
failWaitOnce(error) {
nextWait = error
},
async submit(bytes, options) {
const accepted = await inner.submit(bytes, options)
// The same key is the same job on the platform; a repeat is not a second submission.
if (!submitted.some((entry) => entry.jobId === accepted.jobId)) {
submitted.push({ jobId: accepted.jobId, filename: options.filename, idempotencyKey: options.idempotencyKey })
}
return accepted
},
page: (jobId, page, options) => inner.page(jobId, page, options),
async wait(jobId, options) {
waited.push(jobId)
if (nextWait) {
const error = nextWait
nextWait = undefined
throw error
}
return inner.wait(jobId, options)
},
}
}
/** A fake platform that reads every document as `document`, and the app-side OCR port over it. */
export async function startOcr(
document: Record<string, unknown>,
): Promise<{ platform: FakePlatform; ocr: ObservedOcr }> {
const platform = await startFakePlatform()
platform.ocr.setDocument(document)
const reader = createDocumentReader({ CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: "deployment-token" })
return { platform, ocr: observed(sdkDocumentOcr(reader, { intervalMs: 1 })) }
}
+254
View File
@@ -0,0 +1,254 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The web process's tenancy rules around a request's caller (apps/web/src/server/tenancy-calls.ts,
// ADR-062), over the app's real registry and kit-tenancy's procedures:
//
// - an invitation, once committed, admits its address through @conusai-kit/auth's real platform client
// against the fake admission endpoint of @conusai-kit/testing, audited with ids only
// - with no admission credential the invitation still answers, and the skip is logged with its reason
// - a refused or unreachable admission is logged and audited as a failure, never thrown to the inviter
// - an invitation that is refused (a role above the inviter's) asks for no admission at all
// - with self-service off, only the app-access role `admin` founds an organization; a token without the
// claim is not `admin`
// - on the Convex profile a grant-changing write is announced to the organization's streams here
import { createPlatformIdentityAdmission } from "@conusai-kit/auth"
import { Container } from "@conusai-kit/core"
import { GRANTS_CHANGED } from "@conusai-kit/tenancy"
import { fakeIdentityAdmission } from "@conusai-kit/testing"
import { describe, expect, it, vi } from "vitest"
import { type TenancyCallOptions, withTenancyCalls } from "../apps/web/src/server/tenancy-calls.js"
import { identityLinkOf, organizationSelfServiceOf } from "../config/src/access.js"
import { makeApp, type Person } from "../modules/account/__tests__/harness.js"
const app = makeApp()
type Audited = Parameters<TenancyCallOptions["audit"]>[1]
function wrapped(person: Person, overrides: Partial<TenancyCallOptions> = {}) {
const audited: Audited[] = []
const log = { info: vi.fn(), warn: vi.fn() }
const caller = withTenancyCalls(person.call(), {
principal: { actor: person.actor },
container: new Container(),
selfServiceOrganizations: true,
admission: () => ({ skipped: "no OIDC_ADMISSION_TOKEN" }),
audit: async (_action, event) => {
audited.push(event)
},
log,
...overrides,
})
return { caller, audited, log }
}
describe("an invitation's admission (ADR-062 decision 4)", async () => {
const ada = await app.signUp("ada-admission@example.test")
await ada.call().callByName("tenancy.org.create", { slug: "acme-admission", name: "Acme" })
it("admits the invited address once the invitation committed, and audits ids only", async () => {
const platform = fakeIdentityAdmission()
const admission = createPlatformIdentityAdmission({
apiUrl: platform.apiUrl,
linkId: platform.linkId,
token: platform.token,
fetch: platform.fetch,
})
const { caller, audited, log } = wrapped(ada, { admission: () => admission })
const invitation = (await caller.callByName("tenancy.member.invite", {
org: "acme-admission",
email: " New.Person@Example.test ",
role: "member",
})) as { invitationId: string; token: string }
expect(invitation.token.length).toBeGreaterThan(16)
expect(platform.admissions).toEqual([
expect.objectContaining({ email: "new.person@example.test", invitationId: invitation.invitationId }),
])
expect(platform.calls[0]).toMatchObject({ method: "POST", credential: "valid", status: 202 })
expect(audited).toEqual([{ orgSlug: "acme-admission", invitationId: invitation.invitationId, outcome: "success" }])
// Neither the log nor the audit event carries the address.
expect(JSON.stringify([log.info.mock.calls, log.warn.mock.calls, audited])).not.toContain("example.test")
})
it("answers the invitation without an admission credential, and logs why it skipped", async () => {
const { caller, audited, log } = wrapped(ada)
const invitation = (await caller.callByName("tenancy.member.invite", {
org: "acme-admission",
email: "skipped@example.test",
role: "viewer",
})) as { invitationId: string }
expect(invitation.invitationId).toBeTruthy()
expect(audited).toEqual([])
expect(log.info).toHaveBeenCalledWith("invitation: admission skipped: no OIDC_ADMISSION_TOKEN", {
"conusai.invitation_id": invitation.invitationId,
})
})
it("answers the invitation when the admission client cannot be built", async () => {
const { caller, log } = wrapped(ada, {
admission: () => {
throw new Error("set OIDC_AUDIENCE")
},
})
const invitation = (await caller.callByName("tenancy.member.invite", {
org: "acme-admission",
email: "unbuilt@example.test",
role: "viewer",
})) as { invitationId: string }
expect(log.info).toHaveBeenCalledWith(
"invitation: admission skipped: the admission client could not be built (Error)",
{
"conusai.invitation_id": invitation.invitationId,
},
)
})
it.each([
[429, "rate_limited"],
[401, "unauthorized"],
[503, "unavailable"],
["network", "unavailable"],
] as const)(
"keeps the invitation when the platform answers %s, logged and audited as %s",
async (refusal, reason) => {
const platform = fakeIdentityAdmission()
platform.refuseNext(refusal)
const admission = createPlatformIdentityAdmission({
apiUrl: platform.apiUrl,
linkId: platform.linkId,
token: platform.token,
fetch: platform.fetch,
})
const { caller, audited, log } = wrapped(ada, { admission: () => admission })
const email = `refused-${String(refusal)}@example.test`
const invitation = (await caller.callByName("tenancy.member.invite", {
org: "acme-admission",
email,
role: "viewer",
})) as { invitationId: string }
expect(invitation.invitationId).toBeTruthy()
expect(platform.admissions).toEqual([])
expect(log.warn).toHaveBeenCalledWith(`invitation: admission failed: ${reason}`, {
"conusai.invitation_id": invitation.invitationId,
})
expect(audited).toEqual([
{ orgSlug: "acme-admission", invitationId: invitation.invitationId, outcome: "failure", reason },
])
},
)
it("asks for no admission when the invitation itself is refused", async () => {
const grace = await app.signUp("grace-admission@example.test")
const joined = (await ada.call().callByName("tenancy.member.invite", {
org: "acme-admission",
email: grace.email,
role: "admin",
})) as { token: string }
await grace.call().callByName("tenancy.invitation.accept", { org: "acme-admission", token: joined.token })
const platform = fakeIdentityAdmission()
const admission = createPlatformIdentityAdmission({
apiUrl: platform.apiUrl,
linkId: platform.linkId,
token: platform.token,
fetch: platform.fetch,
})
const { caller, audited } = wrapped(grace, { admission: () => admission })
// An administrator cannot invite an owner (kit-tenancy's ceiling): nothing committed, nothing admitted.
await expect(
caller.callByName("tenancy.member.invite", { org: "acme-admission", email: "owner@example.test", role: "owner" }),
).rejects.toMatchObject({ status: 403 })
expect(platform.calls).toEqual([])
expect(audited).toEqual([])
})
})
describe("founding an organization when self-service is off (ADR-062 decision 5)", async () => {
const linus = await app.signUp("linus-founder@example.test")
it("refuses a person without the app-access role, and one whose token carries no claim", async () => {
for (const principal of [{ actor: linus.actor }, { actor: linus.actor, appAccess: { role: "user" as const } }]) {
const { caller } = wrapped(linus, { selfServiceOrganizations: false, principal })
await expect(
caller.callByName("tenancy.org.create", { slug: "refused-founder", name: "Refused" }),
).rejects.toMatchObject({ status: 403, code: "forbidden" })
}
// Nothing was written: the slug is still free for the operator below.
expect(
((await linus.call().callByName("tenancy.me.get", undefined)) as { organizations: unknown[] }).organizations,
).toEqual([])
})
it("lets the app-access role admin found one, and anyone when self-service is on", async () => {
const admin = wrapped(linus, {
selfServiceOrganizations: false,
principal: { actor: linus.actor, appAccess: { role: "admin" } },
})
await expect(
admin.caller.callByName("tenancy.org.create", { slug: "refused-founder", name: "Ops" }),
).resolves.toMatchObject({
slug: "refused-founder",
})
const open = wrapped(linus, { selfServiceOrganizations: true })
await expect(
open.caller.callByName("tenancy.org.create", { slug: "self-founded", name: "Self" }),
).resolves.toMatchObject({
slug: "self-founded",
})
})
})
describe("a grant change made in the Convex deployment", async () => {
const ada = await app.signUp("ada-remote-grants@example.test")
await ada.call().callByName("tenancy.org.create", { slug: "acme-remote", name: "Acme" })
const grace = await app.signUp("grace-remote-grants@example.test")
const invitation = (await ada.call().callByName("tenancy.member.invite", {
org: "acme-remote",
email: grace.email,
role: "member",
})) as { token: string }
await grace.call().callByName("tenancy.invitation.accept", { org: "acme-remote", token: invitation.token })
it("is announced to this process's streams of the organization, and only of a streamed one", async () => {
const container = new Container()
const announced: string[] = []
container.value(GRANTS_CHANGED, { grantsChanged: async (orgId) => void announced.push(orgId) })
const orgIdOf = (slug: string) => (slug === "acme-remote" ? "org-id-of-acme" : undefined)
const { caller } = wrapped(ada, { container, remoteGrants: { orgIdOf } })
await caller.callByName("tenancy.member.change-role", {
org: "acme-remote",
userId: grace.actor.userId,
role: "viewer",
})
expect(announced).toEqual(["org-id-of-acme"])
// A read is no grant change.
await caller.callByName("tenancy.member.list", { org: "acme-remote" })
expect(announced).toEqual(["org-id-of-acme"])
})
})
describe("the access settings of the deployment", () => {
it("reads APP_ORGANIZATION_SELF_SERVICE as true by default and refuses anything but true or false", () => {
expect(organizationSelfServiceOf({})).toBe(true)
expect(organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "" })).toBe(true)
expect(organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "false" })).toBe(false)
expect(() => organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "no" })).toThrow(
"set APP_ORGANIZATION_SELF_SERVICE to true or false",
)
})
it("takes the identity link with its admission credential, skips without both, and refuses one of the two", () => {
expect(identityLinkOf({})).toEqual({
skipped: "no OIDC_ADMISSION_TOKEN: the identity link issued no admission credential to this app",
})
expect(identityLinkOf({ OIDC_LINK_ID: "4f1c2a7e-6b1d", OIDC_ADMISSION_TOKEN: "cai_adm_x" })).toEqual({
linkId: "4f1c2a7e-6b1d",
})
for (const env of [{ OIDC_LINK_ID: "4f1c" }, { OIDC_ADMISSION_TOKEN: "cai_adm_secret" }]) {
expect(() => identityLinkOf(env)).toThrow("set OIDC_LINK_ID and OIDC_ADMISSION_TOKEN together")
}
// The credential is never quoted back.
expect(() => identityLinkOf({ OIDC_ADMISSION_TOKEN: "cai_adm_secret" })).not.toThrow(/cai_adm_secret/)
expect(() => identityLinkOf({ OIDC_LINK_ID: "../other", OIDC_ADMISSION_TOKEN: "t" })).toThrow(
"set OIDC_LINK_ID to the identity link's id",
)
})
})
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Two people's first paints, one server process. `useCan` caches a person's grants on the data client, so
// a client shared across renders would answer the second person with the first person's grants.
import { Can } from "@conusai-kit/ui/data"
import { createElement } from "react"
import { renderToString } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { AppTransport } from "../apps/web/src/ui/transport.js"
const scope = { org: "acme", workspace: "main" }
const firstPaint = (permissions: readonly `${string}.${string}.${string}`[]): string =>
renderToString(
createElement(AppTransport, {
permissions: { scope, seed: { scope, permissions: [...permissions], revision: "1" } },
children: createElement(Can, { permission: "notes.note.write", fallback: "reads only", children: "writes" }),
}),
)
describe("a server render's permissions", () => {
it("are the person's own, whoever the same process rendered first", () => {
expect(firstPaint(["notes.note.write"])).toContain("writes")
expect(firstPaint([])).toContain("reads only")
expect(firstPaint(["notes.note.write"])).toContain("writes")
})
})
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.paths.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"types": ["node"],
"noEmit": true,
"lib": ["ES2023", "dom"]
},
"include": ["**/*.ts", "../vitest.config.ts", "../scripts/*.cjs"],
"//": "The tests read browser-facing modules as well as server ones, so the DOM library is in scope here even though nothing in test/ runs in a browser."
}
+97
View File
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// `ConusAI-Txid` (umbrella TS6.3): a write's answer carries the transaction id it committed under,
// which the browser's live collections await before they let go of an optimistic row; a read, a
// write that committed nothing and a refused write carry none. The kernel's caller is stubbed with
// one that commits the way kit-data-kysely does inside a capture (`recordTxid` after the commit);
// the capture itself is proven against a real Postgres in @conusai-kit/data-kysely's own gate.
import type { Principal } from "@conusai-kit/auth"
import { AppError } from "@conusai-kit/core"
import { recordTxid } from "@conusai-kit/data-kysely"
import { describe, expect, it, vi } from "vitest"
const route = (kind: "read" | "write", scope: "project" | "user" = "project") => ({
module: "notes",
kind,
scope,
permission: "notes.note.read",
idempotent: false,
tool: false,
download: false,
})
vi.mock("../apps/web/src/server/app", () => ({
convexCapabilityStatus: async () => ({ ready: true }),
webRuntime: () => ({
registry: {
router: {
"notes.note.list": route("read"),
"notes.note.create": route("write"),
"notes.note.noop": route("write"),
"notes.note.refused": route("write"),
},
},
platform: { audit: { log: async () => undefined }, usage: { track: () => undefined } },
observe: {
span: (_name: string, _attributes: unknown, run: () => unknown) => run(),
log: { info: () => undefined, error: () => undefined },
captureException: () => undefined,
},
}),
}))
const principal: Principal = { actor: { userId: "u-txid" }, method: "bearer" } as Principal
vi.mock("../apps/web/src/server/request", () => ({
requestContext: async () => ({
principal,
caller: {
callByName: async (name: string) => {
// Two commits, as a write with an outbox event in a second transaction makes: the higher wins.
if (name === "notes.note.create") {
await Promise.resolve()
recordTxid("7001")
recordTxid("7003")
}
if (name === "notes.note.refused") {
recordTxid("9999")
throw new AppError({ type: "/problems/conflict", title: "Conflict", status: 409, code: "conflict" })
}
return { ok: true }
},
},
}),
}))
async function post(name: string): Promise<Response> {
const { handle } = await import("../apps/web/src/server/router")
return handle(
new Request(`https://app.example.test/api/v1/${name}`, {
method: "POST",
headers: { "content-type": "application/json", authorization: "Bearer stub" },
body: JSON.stringify({ org: "o", workspace: "w", project: "p" }),
}),
)
}
describe("ConusAI-Txid", () => {
it("is the highest transaction id a write committed", async () => {
const answer = await post("notes.note.create")
expect(answer.status).toBe(200)
expect(answer.headers.get("conusai-txid")).toBe("7003")
})
it("is absent from a read, from a write that committed nothing and from a refused write", async () => {
expect((await post("notes.note.list")).headers.get("conusai-txid")).toBeNull()
expect((await post("notes.note.noop")).headers.get("conusai-txid")).toBeNull()
const refused = await post("notes.note.refused")
expect(refused.status).toBe(409)
expect(refused.headers.get("conusai-txid")).toBeNull()
})
it("does not leak one request's id into the next", async () => {
const [first, second] = await Promise.all([post("notes.note.create"), post("notes.note.noop")])
expect(first.headers.get("conusai-txid")).toBe("7003")
expect(second.headers.get("conusai-txid")).toBeNull()
})
})
+71
View File
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A screen is a declaration: data from `load`, columns, actions and labels, handed to the kit's patterns
// (docs/how-to/compose-a-page.md in the platform's documentation). So a screen has no styling of its own: no `className`, no
// colour value. React Aria is out of a screen's reach too, by the `ui-comes-from-the-kit` rule of
// scripts/dep-rules.cjs. A screen that is truly domain-specific says so, and why, in its first lines:
// `// kit:custom-ui <why>`. `pnpm boundaries` and this suite read the same marker.
import { readFileSync } from "node:fs"
import { createRequire } from "node:module"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
const require = createRequire(import.meta.url)
const { buildForbidden, listCustomScreens, listScreens } = require("../scripts/dep-rules.cjs") as {
buildForbidden(
moduleIds: string[],
custom?: string[],
): { name: string; from: { path?: string; pathNot?: string | string[] }; to: { path?: string | string[] } }[]
listCustomScreens(root: string): string[]
listScreens(root: string): string[]
}
const root = new URL("..", import.meta.url).pathname
const COLOUR = /["'`](?:#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|oklch\()/
/** What a declared screen may not hold, by file, with the way out in the message. */
function styling(source: string): string[] {
const found: string[] = []
if (/<(?:button|input|select|textarea|table)(?:\s|>)/.test(source))
found.push("raw control: compose kit primitives or patterns")
if (/\bclassName=/.test(source)) found.push("className: use a pattern's slot, or a variant")
if (COLOUR.test(source)) found.push("a colour value: the kit's roles carry every colour")
return found
}
describe("a screen is a declaration", () => {
const custom = new Set(listCustomScreens(root))
it("holds no styling of its own unless it declares itself custom", () => {
const problems = listScreens(root)
.filter((file) => !custom.has(file))
.flatMap((file) =>
styling(readFileSync(join(root, file), "utf8")).map(
(problem) => `${file}: ${problem}, or \`// kit:custom-ui <why>\``,
),
)
expect(problems).toEqual([])
})
it("catches what it means to", () => {
expect(styling(`<p className="x" />`)).toHaveLength(1)
expect(styling(`const c = "#123abc"`)).toHaveLength(1)
expect(styling(`const c = "oklch(0.5 0 0)"`)).toHaveLength(1)
expect(styling(`<a href="#section" />`)).toEqual([])
expect(styling(`<button type="button" />`)).toHaveLength(1)
expect(styling(`<PageHeader title={title} />`)).toEqual([])
})
it("keeps React Aria behind the kit, for every screen that has not declared itself", () => {
const rule = buildForbidden([], ["modules/x/ui/own.tsx"]).find(({ name }) => name === "ui-comes-from-the-kit")
const target = new RegExp(rule?.to.path as string)
expect(target.test("react-aria-components")).toBe(true)
expect(target.test("node_modules/.pnpm/x/node_modules/@react-aria/button/dist/index.js")).toBe(true)
expect(target.test("@conusai-kit/ui/patterns")).toBe(false)
expect(rule?.from.pathNot).toEqual(["^modules/x/ui/own\\.tsx$"])
})
it("leaves custom screens the exception", () => {
expect(custom.size).toBeLessThan(listScreens(root).length / 2)
})
})
+131
View File
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// SIGTERM, which the platform sends before it replaces a container. The real `server.mjs` runs against a stand-in
// for the Start build: the order is stop taking requests, finish the ones in flight, drain the runtime (usage queue,
// exporters, pools), and only then exit. A process that exits on its own signal handler loses the queue.
import { type ChildProcess, spawn } from "node:child_process"
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { afterEach, describe, expect, it } from "vitest"
const WEB = fileURLToPath(new URL("../apps/web/", import.meta.url))
/** The stand-in: `/slow` holds a request, `/start` builds the "runtime", which registers its drain as app.ts does. */
const BUILD = `
import { appendFileSync } from "node:fs"
const log = process.env.SHUTDOWN_LOG
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const note = (line) => appendFileSync(log, line + "\\n")
const drainMs = Number(process.env.SHUTDOWN_DRAIN_MS ?? 300)
export default {
async fetch(request) {
const { pathname } = new URL(request.url)
if (pathname === "/start") {
globalThis[Symbol.for("conusai.app.drain")] = async () => {
note("drain began")
await sleep(drainMs)
note("drain ended")
}
}
if (pathname === "/stream") {
return new Response(new ReadableStream({ start(controller) { controller.enqueue("open\\n") } }))
}
if (pathname === "/slow") {
await sleep(400)
note("request finished")
}
return new Response("ok")
},
}
`
let directory: string | undefined
let server: ChildProcess | undefined
afterEach(() => {
server?.kill("SIGKILL")
if (directory) rmSync(directory, { recursive: true, force: true })
directory = undefined
})
/** Starts the real server.mjs beside the stand-in, inside apps/web so that hono resolves as it does in the image. */
async function boot(
env: Record<string, string> = {},
): Promise<{ origin: string; log: string; exited: Promise<number> }> {
directory = mkdtempSync(join(WEB, ".shutdown-test-"))
mkdirSync(join(directory, "dist", "server"), { recursive: true })
mkdirSync(join(directory, "dist", "client"), { recursive: true })
copyFileSync(join(WEB, "server.mjs"), join(directory, "server.mjs"))
writeFileSync(join(directory, "dist", "server", "server.js"), BUILD)
const log = join(directory, "shutdown.log")
writeFileSync(log, "")
const port = 34_000 + Math.floor(Math.random() * 20_000)
const child = spawn(process.execPath, ["server.mjs", "--port", String(port)], {
cwd: directory,
env: { ...process.env, HOSTNAME: "127.0.0.1", SHUTDOWN_LOG: log, ...env },
stdio: "ignore",
})
server = child
const exited = new Promise<number>((resolve) => child.once("exit", (code) => resolve(code ?? -1)))
const origin = `http://127.0.0.1:${port}`
for (let attempt = 0; attempt < 100; attempt += 1) {
if (
await fetch(`${origin}/health`).then(
(answer) => answer.ok,
() => false,
)
)
return { origin, log, exited }
await new Promise((resolve) => setTimeout(resolve, 50))
}
throw new Error("server.mjs did not start")
}
const lines = (log: string): string[] => readFileSync(log, "utf8").split("\n").filter(Boolean)
describe("the web server on SIGTERM", () => {
it("drains the runtime to its end before the process exits", async () => {
const { origin, log, exited } = await boot()
await fetch(`${origin}/start`)
server?.kill("SIGTERM")
expect(await exited).toBe(0)
expect(lines(log)).toEqual(["drain began", "drain ended"])
}, 15_000)
it("finishes the request in flight before it drains, so its usage is in the queue", async () => {
const { origin, log, exited } = await boot()
await fetch(`${origin}/start`)
const slow = fetch(`${origin}/slow`)
await new Promise((resolve) => setTimeout(resolve, 100))
server?.kill("SIGTERM")
expect((await slow).status).toBe(200)
expect(await exited).toBe(0)
expect(lines(log)).toEqual(["request finished", "drain began", "drain ended"])
}, 15_000)
it("does not let a stream that never ends hold the drain past its bound", async () => {
const { origin, log, exited } = await boot({ SHUTDOWN_REQUESTS_MS: "300" })
await fetch(`${origin}/start`)
const stream = await fetch(`${origin}/stream`)
expect(stream.status).toBe(200)
server?.kill("SIGTERM")
expect(await exited).toBe(0)
expect(lines(log)).toEqual(["drain began", "drain ended"])
}, 15_000)
it("exits at once when no request ever built a runtime", async () => {
const { log, exited } = await boot()
server?.kill("SIGTERM")
expect(await exited).toBe(0)
expect(lines(log)).toEqual([])
}, 15_000)
it("exits non-zero rather than hang when the drain does not finish", async () => {
const { origin, log, exited } = await boot({ SHUTDOWN_DRAIN_MS: "60000", SHUTDOWN_FLUSH_MS: "300" })
await fetch(`${origin}/start`)
server?.kill("SIGTERM")
expect(await exited).toBe(1)
expect(lines(log)).toEqual(["drain began"])
}, 15_000)
})
+140
View File
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A module's pages above a project (F10): `/<org>/<workspace>/modules/<module>/...`. Signed out is a redirect,
// a workspace the person cannot see is the same 404 as one that does not exist, a missing permission is a 403
// before `load` runs, and `load` receives the query string. `e2e/workspace-pages.spec.ts` proves the same four
// answers over a build on a real Postgres; these say which line gives each one.
import { AppError } from "@conusai-kit/core"
import { isNotFound, isRedirect } from "@tanstack/react-router"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
requestContext: vi.fn(),
callByName: vi.fn(),
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
load: vi.fn(),
call: vi.fn(),
}))
vi.mock("@tanstack/react-start/server", () => ({
getRequest: () => {
throw new Error("the tests pass their request")
},
setResponseStatus: vi.fn(),
setResponseHeader: mocks.setResponseHeader,
}))
vi.mock("../apps/web/src/server/request", () => ({ requestContext: mocks.requestContext }))
vi.mock("../apps/web/src/server/app", () => ({
webRuntime: () => ({
observe: { config: { errors: false } },
selfServiceOrganizations: "members",
registry: {
modules: [{ id: "reports" }, { id: "unpaged" }],
nav: [{ module: "reports", scope: "workspace", href: "reports/a/b", label: "workspace.overview" }],
},
env: { deployment: {} },
}),
}))
vi.mock("../apps/web/src/server/tenancy-calls", () => ({ mayCreateOrganization: () => false }))
vi.mock("../apps/web/src/server/platform-theme", () => ({ organizationThemeStyle: async () => undefined }))
vi.mock("@conusai-kit/host/http", () => ({ csrfSessionId: () => "session", csrfToken: () => "token" }))
vi.mock("@app/modules/pages", () => ({
modulePages: {},
workspacePages: {
reports: async () => ({ permission: "reports.report.read", load: mocks.load }),
},
}))
const { workspacePageData } = await import("../apps/web/src/server/page")
const person = { actor: { kind: "user" } }
const signedIn = () =>
mocks.requestContext.mockResolvedValue({
caller: { call: mocks.call, callByName: mocks.callByName },
container: {},
principal: person,
user: { email: "ada@example.test", displayName: "Ada", locale: "en" },
})
const request = (): Request =>
new Request("http://app.test/a/b/modules/reports", { headers: { "x-path": "/a/b/modules/reports" } })
const params = { org: "a", workspace: "b", module: "reports" }
const refusal = (status: number): AppError =>
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
const thrown = (run: Promise<unknown>): Promise<unknown> =>
run.then(
() => "resolved",
(error: unknown) => error,
)
beforeEach(() => {
vi.clearAllMocks()
mocks.callByName.mockResolvedValue({ permissions: ["reports.report.read"], revision: "r1" })
mocks.load.mockResolvedValue({ title: "Reports" })
})
describe("a workspace page's loader", () => {
it("sends a signed-out visitor to sign in, before reading anything", async () => {
mocks.requestContext.mockResolvedValue({ caller: {}, container: {}, principal: null })
expect(isRedirect(await thrown(workspacePageData(params, request())))).toBe(true)
expect(mocks.load).not.toHaveBeenCalled()
})
it("hands `load` the workspace, the path below the module and the query string", async () => {
signedIn()
const data = await workspacePageData({ ...params, _splat: "a/b", search: "?q=one&tab=two&q=three" }, request())
expect(mocks.load).toHaveBeenCalledWith(
expect.objectContaining({
slugs: { org: "a", workspace: "b" },
segments: ["a", "b"],
search: { q: "three", tab: "two" },
}),
)
expect(data).toMatchObject({
module: "reports",
title: "Workspace overview · Example App",
segments: ["a", "b"],
locale: "en",
data: { title: "Reports" },
scope: { org: "a", workspace: "b" },
seed: { permissions: ["reports.report.read"] },
})
})
it("answers 404 for a workspace of another organization, before `load`", async () => {
signedIn()
mocks.callByName.mockRejectedValue(refusal(404))
expect(isNotFound(await thrown(workspacePageData(params, request())))).toBe(true)
expect(mocks.load).not.toHaveBeenCalled()
})
it("answers 403 to a member without the page's permission, before `load`", async () => {
signedIn()
mocks.callByName.mockResolvedValue({ permissions: [], revision: "r1" })
const error = (await thrown(workspacePageData(params, request()))) as { data?: unknown }
expect(isNotFound(error)).toBe(true)
expect(error.data).toEqual({ forbidden: true })
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "403")
expect(mocks.load).not.toHaveBeenCalled()
})
it.each([
["no registered module", { ...params, module: "nothing" }],
["a registered module with no workspace page", { ...params, module: "unpaged" }],
["a name every object has", { ...params, module: "constructor" }],
])("answers 404 for %s", async (_name, wanted) => {
signedIn()
expect(isNotFound(await thrown(workspacePageData(wanted, request())))).toBe(true)
expect(mocks.load).not.toHaveBeenCalled()
})
it.each([
[404, undefined],
[403, { forbidden: true }],
])("turns the module's own %i into that page", async (status, expected) => {
signedIn()
mocks.load.mockRejectedValue(refusal(status))
const error = (await thrown(workspacePageData(params, request()))) as { data?: unknown }
expect(isNotFound(error)).toBe(true)
expect(error.data).toEqual(expected)
})
})