// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 import { readFileSync } from "node:fs" import { AppError, buildRegistry } from "@conusai-kit/core" import { generateConvex } from "@conusai-kit/data-convex" import { getFunctionName } from "convex/server" import { ConvexError } from "convex/values" import { describe, expect, it } from "vitest" import { parse } from "yaml" import { projectEntriesFor } from "../apps/web/src/ui/projects.js" 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", () => { 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 })', ) }) it("keeps the documents review route visible in the Convex project navigation", () => { expect(projectEntriesFor("convex")).toContainEqual({ href: "documents", label: "documents.title", module: "documents", }) }) 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) const closure = ["tenancy", "auth", "files", "proposals"] closure.push("guides") // kit:guides -- its effect procedures are generated as Convex actions closure.push("support") // kit:support -- its effect procedures are generated as Convex actions expect(modules.map(({ id }) => id)).toEqual([...closure, "branding", "documents", "notes", "account"]) // 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"] // 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", ) expect(bff.find(({ id }) => id === "documents")?.procedures?.map(({ name }) => name)).toContain( "documents.document.get", ) }) 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 restart?: string depends_on?: Record } > } const profile = JSON.parse(readFileSync(new URL("profiles/convex/conusai.json", root), "utf8")) as { services: Record } 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}") expect(Object.keys(environment).some((key) => key.startsWith("NEXT_PUBLIC_") && 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('"env", "set", "--from-file", envFile') expect(deploy).toContain("mode: 0o600") expect(deploy).toContain('"deploy", "--typecheck", "disable"') const runtime = readFileSync(new URL("convex/lib/runtime.ts", root), "utf8") expect(runtime).toContain('"filesActions:recoverUpload"') }) }) 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("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) => ({ 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) => ({ 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("delivered from an action's container, against the fake platform", () => { const START = Date.parse("2026-10-25T10:00:00.000Z") type Platform = Awaited> 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): Promise { 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 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 })