import { describe, expect, test } from "bun:test"; import { createCapabilityToken } from "./capability.ts"; import { createEffectLedger, reversibilityOf } from "./effects.ts"; import { fail, formatOkeMessage, lookupOkeError, OKE_ERRORS, OkeError } from "./errors.ts"; import { createFx, createFxContext, jsonResultBrand, type Fx, type FxStubStoreHandle, type NamedRef, } from "./fx.ts"; import { Redacted } from "./redacted.ts"; /** Narrow stub handle for tests that exercise the in-memory store. */ function stub(fx: Fx, ref: string): FxStubStoreHandle { return fx.store(ref) as FxStubStoreHandle; } describe("fx — capability enforcement", () => { test("undeclared store read throws OKE1001 with flow, resource, and fix", async () => { const { fx } = createFxContext({ flow: "bookings.create", effects: { reads: ["sql:bookings"] }, }); let err: unknown; try { await stub(fx, "sql:users").get("u1"); } catch (e) { err = e; } expect(err).toBeInstanceOf(OkeError); const oke = err as OkeError; expect(oke.code).toBe(1001); expect(oke.causeText).toBe('Flow "bookings.create" reads "sql:users" without declaring it.'); expect(oke.fix).toBe('Add "sql:users" to this flow\'s effects.reads.'); expect(oke.docsUrl).toBe("https://oke.omqkhafi.dev/e/1001"); expect(oke.message).toBe(formatOkeMessage(1001, oke.causeText, oke.fix, oke.docsUrl)); }); test("SQL table with a name column gates as sql:, not sql:", async () => { const { createStoreRuntime, field, store } = await import("../elements/store.ts"); const { memorySqlDriver } = await import("../drivers/index.ts"); const teams = store.schema.table("teams", { id: field.text().primaryKey(), name: field.text().notNull(), }); const decl = store.sql("db", { schema: { teams } }); const rt = createStoreRuntime({ drivers: { sql: memorySqlDriver }, sql: { db: { name: "db", primary: {} } }, }); rt.register(decl); const fx = createFx({ flow: "teams.list", effects: { reads: ["sql:teams"], writes: ["sql:teams"] }, storeRuntime: rt, }); await fx.store(decl).insert(teams).values({ id: "t1", name: "Eng" }); const rows = await fx.store(decl).select().from(teams); expect(rows).toEqual([{ id: "t1", name: "Eng" }]); }); test("kv ttlMs is a read of kv:", async () => { const { createStoreRuntime, store } = await import("../elements/store.ts"); const { memoryKvDriver } = await import("../drivers/index.ts"); const decl = store.kv("drafts"); const rt = createStoreRuntime({ drivers: { kv: memoryKvDriver }, kv: { drafts: {} }, }); rt.register(decl); const writer = createFx({ flow: "drafts.save", effects: { writes: ["kv:drafts"] }, storeRuntime: rt, }); await writer.store(decl).set("ENG-1", { title: "Draft" }, "7d"); const reader = createFx({ flow: "drafts.expire", effects: { reads: ["kv:drafts"] }, storeRuntime: rt, }); const ttl = await reader.store(decl).ttlMs("ENG-1"); expect(ttl).toBeGreaterThan(0); }); test("kv prefixes keys with tenant id and fails loud without one", async () => { const { createStoreRuntime, store } = await import("../elements/store.ts"); const { memoryKvDriver } = await import("../drivers/index.ts"); const decl = store.kv("drafts"); const rt = createStoreRuntime({ drivers: { kv: memoryKvDriver }, kv: { drafts: {} }, }); rt.register(decl); const acme = createFx({ flow: "drafts.acme", effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] }, storeRuntime: rt, tenantEnabled: true, tenant: { id: "acme" }, }); const globex = createFx({ flow: "drafts.globex", effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] }, storeRuntime: rt, tenantEnabled: true, tenant: { id: "globex" }, }); await acme.store(decl).set("note", "alice"); await globex.store(decl).set("note", "bob"); expect(await acme.store(decl).get("note")).toBe("alice"); expect(await globex.store(decl).get("note")).toBe("bob"); expect(await acme.store(decl).list()).toEqual(["note"]); const none = createFx({ flow: "drafts.none", effects: { writes: ["kv:drafts"] }, storeRuntime: rt, tenantEnabled: true, tenant: { id: null }, }); let err: unknown; try { await none.store(decl).set("note", "x"); } catch (e) { err = e; } expect(err).toBeInstanceOf(OkeError); expect((err as OkeError).code).toBe(1810); }); test("index driverId is meilisearch before first I/O (not a function, not memory)", async () => { const { createStoreRuntime, store } = await import("../elements/store.ts"); const { meilisearchDriver } = await import("../drivers/index.ts"); const decl = store.index("tasks"); const rt = createStoreRuntime({ drivers: { index: meilisearchDriver }, index: { tasks: { url: "http://127.0.0.1:7700" } }, }); rt.register(decl); const fx = createFx({ flow: "search.seed", effects: { writes: ["index:tasks"] }, storeRuntime: rt, }); expect(fx.store(decl).driverId).toBe("meilisearch"); }); test("declared store read succeeds", async () => { const fx = createFx({ flow: "bookings.create", effects: { reads: ["sql:bookings"] }, storeData: { "sql:bookings": { b1: { id: "b1" } } }, }); await expect(stub(fx, "sql:bookings").get("b1")).resolves.toEqual({ id: "b1", }); }); }); describe("fx — effect ledger", () => { test("read, write, emit → exactly three ledger entries in order", async () => { const ledger = createEffectLedger(); const fx = createFx({ flow: "bookings.create", effects: { reads: ["sql:bookings"], writes: ["sql:bookings"], emits: ["order-placed"], }, ledger, }); await stub(fx, "sql:bookings").get("b1"); await stub(fx, "sql:bookings").insert({ seats: 2 }); await fx.emit("order-placed", { orderId: "o1" }); expect(ledger.entries).toHaveLength(3); expect(ledger.entries.map((e) => e.kind)).toEqual(["read", "write", "emit"]); expect(ledger.entries.map((e) => e.resource)).toEqual([ "sql:bookings", "sql:bookings", "order-placed", ]); expect(ledger.entries.map((e) => e.reversibility)).toEqual(["none", "reversible", "deferred"]); }); test("all seven kinds receive the correct reversibility tier via fx", async () => { const ledger = createEffectLedger(); const fx = createFx({ flow: "kitchen.sink", effects: { reads: ["sql:bookings"], writes: ["sql:bookings"], emits: ["order-placed"], sends: ["order-confirmed"], asks: ["triage@1"], secrets: ["STRIPE_KEY"], calls: ["payments.charge"], }, ledger, secrets: { STRIPE_KEY: "sk_test" }, aiRuntime: { prompts: new Map(), agents: new Map(), embeds: new Map(), autoCacheDisabled: true, journalingForced: false, denials: [], agentRuns: [], journal: [], async ask() { return { ok: true }; }, async runAgent() { return { ok: true, steps: 0, denials: [], output: {} }; }, async *stream() { /* no tokens */ }, async search() { return []; }, async embed() { return { vectors: [] }; }, } as never, }); await stub(fx, "sql:bookings").get("x"); await stub(fx, "sql:bookings").set("x", { ok: true }); await fx.emit("order-placed", {}); await fx.send("order-confirmed", { to: "u1" }); await fx.ask("triage@1", {}); expect((await fx.vault.get("STRIPE_KEY")).reveal()).toBe("sk_test"); await fx.call("payments.charge", {}); expect(ledger.entries).toHaveLength(7); const kinds = ledger.entries.map((e) => e.kind); expect(kinds).toEqual(["read", "write", "emit", "send", "ask", "secret", "call"]); for (const entry of ledger.entries) { expect(entry.reversibility).toBe(reversibilityOf(entry.kind)); } }); test("fx.ask with a prompt handle matches effects.asks name@version", async () => { const fx = createFx({ flow: "documents.summarize", effects: { asks: ["document-summary@1"] }, aiRuntime: { prompts: new Map(), agents: new Map(), embeds: new Map(), autoCacheDisabled: true, journalingForced: false, denials: [], agentRuns: [], journal: [], async ask() { return { summary: "ok" }; }, async runAgent() { return { ok: true, steps: 0, denials: [], output: {} }; }, async *stream() { /* no tokens */ }, async search() { return []; }, async embed() { return { vectors: [] }; }, } as never, }); await expect(fx.ask({ name: "document-summary", version: 1 }, {})).resolves.toEqual({ summary: "ok", }); }); }); describe("fx.vault — object surface", () => { /** Minimal in-memory adapter covering the surface `fx.vault` uses. */ function memoryAdapter() { const rows = new Map(); const actors: string[] = []; return { rows, actors, adapter: { id: "memory", async get(path: string) { const row = rows.get(path); return row === undefined ? undefined : { path, ...row }; }, async set(path: string, value: string, opts?: { actor?: { id?: string } }) { if (opts?.actor?.id) actors.push(opts.actor.id); const version = (rows.get(path)?.version ?? 0) + 1; rows.set(path, { value, version }); return { path, value, version }; }, async rotate(path: string, value: string) { const version = (rows.get(path)?.version ?? 0) + 1; rows.set(path, { value, version }); return { path, value, version }; }, async delete(path: string) { return rows.delete(path); }, async list(opts?: { prefix?: string }) { return [...rows.keys()] .filter((p) => opts?.prefix === undefined || p.startsWith(opts.prefix)) .map((path) => ({ path, version: rows.get(path)!.version })); }, async status() { return { sealed: false, initialized: true, secretCount: rows.size }; }, }, }; } test("mutations run through the adapter, ledger `secret`, and carry the flow actor", async () => { const { adapter, actors, rows } = memoryAdapter(); const ledger = createEffectLedger(); const fx = createFx({ flow: "ops.rotateKey", effects: { secrets: ["prod/api/stripe"] }, ledger, // The adapter's structural shape is all `fx.vault` consumes. vaultAdapter: adapter as never, }); expect(await fx.vault.set("prod/api/stripe", "sk_1")).toEqual({ path: "prod/api/stripe", version: 1, }); expect(await fx.vault.rotate("prod/api/stripe", "sk_2")).toEqual({ path: "prod/api/stripe", version: 2, }); expect(await fx.vault.list("prod/")).toEqual(["prod/api/stripe"]); expect(await fx.vault.status()).toEqual({ sealed: false, initialized: true, backend: "memory", }); expect(await fx.vault.delete("prod/api/stripe")).toBe(true); expect(rows.size).toBe(0); expect(actors).toEqual(["ops.rotateKey"]); expect(ledger.entries.map((e) => e.kind)).toEqual(["secret", "secret", "secret"]); }); test("an undeclared path is refused before the adapter is touched", async () => { const { adapter, rows } = memoryAdapter(); const fx = createFx({ flow: "ops.rotateKey", effects: { secrets: ["prod/api/stripe"] }, vaultAdapter: adapter as never, }); await expect(fx.vault.set("prod/api/other", "sk_1")).rejects.toThrow(/OKE1006/); expect(rows.size).toBe(0); }); test("mutations without a bound adapter name the missing backend", async () => { const fx = createFx({ flow: "ops.rotateKey", effects: { secrets: ["prod/api/stripe"] }, }); await expect(fx.vault.set("prod/api/stripe", "sk_1")).rejects.toThrow( /fx\.vault\.set needs a bound Vault backend/, ); await expect(fx.vault.status()).rejects.toThrow( /fx\.vault\.status needs a bound Vault backend/, ); }); test("per-tenant contracts store under {tenantId}/{contract}", async () => { const { createVaultRuntime, vault } = await import("../elements/vault.ts"); const { adapter, rows } = memoryAdapter(); const stripe = vault.secret("STRIPE_KEY", { perTenant: true, dev: "sk_unused" }); const runtime = createVaultRuntime({ secrets: [stripe], tenantEnabled: true, }); const fx = createFx({ flow: "billing.key", effects: { secrets: ["STRIPE_KEY"] }, vaultRuntime: runtime, vaultAdapter: adapter as never, tenantEnabled: true, tenant: { id: "acme" }, }); await fx.vault.set("STRIPE_KEY", "sk_acme"); expect([...rows.keys()]).toEqual(["acme/STRIPE_KEY"]); const redacted = await fx.vault.get("STRIPE_KEY"); expect(redacted.reveal()).toBe("sk_acme"); }); }); describe("fx — wholesale swap", () => { test("fx is swappable wholesale (plain object interface)", async () => { const calls: string[] = []; const fake: Fx = { store() { throw new Error("store must not be used"); }, runs: { query: async () => { throw new Error("runs must not be used"); }, all: async () => { throw new Error("runs must not be used"); }, window: async () => { throw new Error("runs must not be used"); }, checkSlo: async () => { throw new Error("runs must not be used"); }, }, async emit(signal: NamedRef) { calls.push(`emit:${typeof signal === "string" ? signal : signal.name}`); }, async deadLetters() { return []; }, live() { throw new Error("live must not be used"); }, call: (async () => null) as Fx["call"], clock: { now: () => 0, ago: () => 0, fromNow: () => 0, duration: () => 0, sleep: async () => undefined, }, vault: { async get() { return new Redacted(""); }, async set() { throw new Error("vault.set must not be used"); }, async rotate() { throw new Error("vault.rotate must not be used"); }, async delete() { throw new Error("vault.delete must not be used"); }, async list() { return []; }, async status() { return { sealed: false, initialized: true, backend: "memory" }; }, }, cache: { get: async () => undefined, set: async () => undefined, getOrSet: async (_k, _t, produce) => produce(), }, async send() { return { ok: true }; }, async sendOtp() { return { ok: true }; }, async verifyOtp() { return { ok: true }; }, async deliverOtp(opts) { return { ok: true as const, channel: opts.only ?? opts.channels[0] ?? "email" }; }, ask: (async () => ({})) as unknown as Fx["ask"], async embed() { return []; }, async search() { return []; }, async fetch() { return new Response(null, { status: 204 }); }, async decide(): Promise { return {} as T; }, agent: { async approve() { return { ok: true as const }; }, async deny() { return { ok: true as const }; }, }, run: (async () => ({})) as unknown as Fx["run"], async *stream() { /* empty */ }, log: { debug() {}, info() {}, warn() {}, error() {}, }, t: (key) => key, locale: "en", id: () => "fixed-id", auth: { userId: "u1", scopes: new Set(["a"]), createApiKey: async () => { throw new Error("auth.createApiKey must not be used"); }, listApiKeys: async () => [], revokeApiKey: async () => { throw new Error("auth.revokeApiKey must not be used"); }, rotateApiKey: async () => { throw new Error("auth.rotateApiKey must not be used"); }, updateApiKey: async () => { throw new Error("auth.updateApiKey must not be used"); }, listTenants: async () => [], switchTenant: async () => { throw new Error("auth.switchTenant must not be used"); }, createTenant: async () => { throw new Error("auth.createTenant must not be used"); }, deleteTenant: async () => { throw new Error("auth.deleteTenant must not be used"); }, addMember: async () => { throw new Error("auth.addMember must not be used"); }, removeMember: async () => { throw new Error("auth.removeMember must not be used"); }, listMembers: async () => [], upsertTenantRole: async () => { throw new Error("auth.upsertTenantRole must not be used"); }, }, operator: { id: null }, principal: { userId: "u1", operatorId: null, scopes: new Set(["a"]) }, tenant: { id: "t1" }, fail, json: { ok(value, opts) { return { [jsonResultBrand]: true, status: 200, value, meta: opts?.meta }; }, create(value) { return { [jsonResultBrand]: true, status: 201, value }; }, empty() { return { [jsonResultBrand]: true, status: 204 }; }, with(dataOrPage: unknown, meta?: Record) { if (meta !== undefined) { return { [jsonResultBrand]: true, status: 200, value: dataOrPage, meta }; } const page = dataOrPage as { data: unknown; meta: Record }; return { [jsonResultBrand]: true, status: 200, value: page.data, meta: page.meta }; }, withQuery(rows) { return { [jsonResultBrand]: true, status: 200, value: [...rows], meta: {} }; }, stream(chunks) { return { [jsonResultBrand]: true, kind: "stream" as const, status: 200 as const, chunks }; }, }, async step(_name, fn) { return fn(); }, get signal() { return new AbortController().signal; }, rlsIdentity: undefined, all(thunks) { return Promise.all(thunks.map((t) => t())) as never; }, race(thunks) { return Promise.race(thunks.map((t) => t())); }, retry(fn) { return Promise.resolve().then(fn); }, async using(acquire, release, use) { const resource = await acquire(); try { return await use(resource); } finally { await release(resource); } }, }; // Any code that accepts `Fx` can run against a total replacement. async function runFlow(fx: Fx): Promise { await fx.emit("order-placed", { id: fx.id() }); return fx.t("hello"); } await expect(runFlow(fake)).resolves.toBe("hello"); expect(calls).toEqual(["emit:order-placed"]); }); }); describe("fx.t — catalogs", () => { test("resolves locale then default with ICU values", () => { const fx = createFx({ flow: "x", effects: {}, i18n: { locale: "ar", defaultLocale: "en", catalogs: { en: { "errors.notFound": "Not found", greeting: "Hello, {name}", items: "{count, plural, one {# item} other {# items}}", }, ar: { greeting: "مرحباً، {name}" }, }, }, }); expect(fx.locale).toBe("ar"); expect(fx.t("greeting", { name: "Ada" })).toBe("مرحباً، Ada"); expect(fx.t("errors.notFound")).toBe("Not found"); expect(fx.t("items", { count: 2 })).toBe("2 items"); expect(fx.t("missing")).toBe("missing"); }); }); describe("fx.clock — relative instants", () => { test("ago / fromNow / duration use the injected now and duration strings", () => { const fx = createFx({ flow: "sessions.sweepExpired", effects: {}, now: () => 1_000_000 }); expect(fx.clock.now()).toBe(1_000_000); expect(fx.clock.duration("30d")).toBe(30 * 86_400_000); expect(fx.clock.ago("30d")).toBe(1_000_000 - 30 * 86_400_000); expect(fx.clock.fromNow("14d")).toBe(1_000_000 + 14 * 86_400_000); expect(fx.clock.duration("nope")).toBe(0); expect(fx.clock.ago("nope")).toBe(1_000_000); expect(fx.clock.fromNow("200ms")).toBe(1_000_200); }); }); describe("fx.json", () => { test("with accepts a page or (data, meta)", () => { const fx = createFx({ flow: "x", effects: {} }); const items = [{ id: "n_1" }]; const meta = { mode: "offset", total: 1, limit: 1, offset: 0 }; const fromPage = fx.json.with({ data: items, meta }); const fromArgs = fx.json.with(items, meta); expect(fromPage).toMatchObject({ status: 200, value: items, meta }); expect(fromArgs).toMatchObject({ status: 200, value: items, meta }); expect(() => fx.json.with(items as never)).toThrow(/pass \(data, meta\)/); }); test("withQuery pages rows and skips auto-eq on id", () => { const fx = createFx({ flow: "x", effects: {} }); const rows = [ { id: "c_1", issueId: "iss_1", title: "Ship" }, { id: "c_2", issueId: "iss_1", title: "Later" }, { id: "c_3", issueId: "iss_2", title: "Ship docs" }, ]; const page = fx.json.withQuery(rows, { id: "c_1", q: "ship", limit: 10 }); expect(page).toMatchObject({ status: 200, value: [ { id: "c_1", issueId: "iss_1", title: "Ship" }, { id: "c_3", issueId: "iss_2", title: "Ship docs" }, ], }); expect(page.meta).toMatchObject({ mode: "offset", total: 2, next: null }); const filtered = fx.json.withQuery(rows, { issueId: "iss_1", limit: 10 }); expect(filtered.value).toEqual(rows.slice(0, 2)); }); }); describe("errors — registry", () => { test("OKE1240 is reserved with the §21 shape", () => { const def = OKE_ERRORS.ORPHAN_EMIT; expect(def.code).toBe(1240); expect(lookupOkeError(1240)).toEqual(def); const err = new OkeError(def, { flow: "bookings.create", resource: "order-placed", }); expect(err.message).toContain("OKE1240"); expect(err.message).toContain("no subscriber"); expect(err.message).toContain("https://oke.omqkhafi.dev/e/1240"); expect(err.message).toContain("→"); }); test("OKE1250 is reserved for signal schema emit failures", () => { const def = OKE_ERRORS.SIGNAL_SCHEMA; expect(def.code).toBe(1250); expect(lookupOkeError(1250)).toEqual(def); const err = new OkeError(def, { resource: "order-placed", detail: "total: Expected number, received string", }); expect(err.message).toContain("OKE1250"); expect(err.message).toContain("order-placed"); expect(err.message).toContain("https://oke.omqkhafi.dev/e/1250"); }); test("fx.fail returns a value, not an exception", () => { const fx = createFx({ flow: "x", effects: {} }); const result = fx.fail( "FlightFull", { seatsLeft: 0 }, { message: "full", }, ); expect(result).toEqual({ data: null, error: { code: "FlightFull", data: { seatsLeft: 0 }, message: "full" }, }); }); test("fx.fail.notFound is callable without errors: declaration", () => { const fx = createFx({ flow: "x", effects: {} }); const result = fx.fail.notFound({ id: "n1" }); expect(result.error.code).toBe("NotFound"); expect(result.error.data).toEqual({ id: "n1" }); expect(result.data).toBeNull(); }); test("capability token allows only declared resources", () => { const token = createCapabilityToken("f", { reads: ["sql:a"], emits: ["s"], }); expect(token.allows("read", "sql:a")).toBe(true); expect(token.allows("read", "sql:b")).toBe(false); expect(() => token.assert("emit", "missing")).toThrow(OkeError); }); });