// 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 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}") // `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 } 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 } 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 } 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) => ({ 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("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> 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 }) // Exercise this closure with bounded, explicit optional admission. vi.mock("../modules/inclusion.js", async (importOriginal) => ({ ...(await importOriginal()), ...(await import("./support/native-readiness.js")).selectedModules(["documents", "guides", "support"]), })) vi.mock("../modules/dist/inclusion.js", async (importOriginal) => ({ ...(await importOriginal()), ...(await import("./support/native-readiness.js")).selectedModules(["documents", "guides", "support"]), }))