import { describe, expect, test, beforeEach } from "bun:test"; import { clock } from "../elements/clock/declare.ts"; import { field, store } from "../elements/store.ts"; import { oke } from "./app.ts"; import { flow, isFlow, resetFlowSeq } from "./flow.ts"; import { on, resetBindings } from "./on.ts"; import { http, internal } from "./triggers.ts"; beforeEach(() => { resetBindings(); resetFlowSeq(); }); describe("flow — one species", () => { test("flow() returns a branded FlowDef", () => { const f = flow("ping", { do: () => ({ ok: true }), }); expect(isFlow(f)).toBe(true); expect(f.name).toBe("ping"); expect(f.triggers).toEqual([]); expect(f.durable).toBe(false); }); test("on() returns the same flow object and records the trigger", () => { const f = flow("create", { do: () => 1 }); const bound = on(http.post("/notes"), f); // Same runtime object; trigger stamp is type-only so identities differ at the type level. expect(bound as object).toBe(f); expect(f.triggers).toHaveLength(1); expect(f.triggers[0]?.kind).toBe("http"); expect(bound.$trigger?.kind).toBe("http"); }); test("the same flow object is reachable from HTTP and signal triggers", async () => { const calls: string[] = []; const shared = flow("shared.work", { do: (input: { n: number }) => { calls.push(`n=${input.n}`); return { n: input.n * 2 }; }, }); const signal = { name: "order-placed", delivery: "once" as const }; on(http.post("/work"), shared); on(signal, shared); const app = oke({ autoBoot: false, name: "one-species" }); const sharedBindings = app.bindings.filter((binding) => binding.flow === shared); expect(sharedBindings).toHaveLength(2); const httpResult = await app.execute(shared, { n: 3 }, shared.triggers[0]!); const signalResults = await app.dispatchSignal("order-placed", { n: 4 }); expect(httpResult.output).toEqual({ n: 6 }); expect(signalResults[0]?.output).toEqual({ n: 8 }); expect(calls).toEqual(["n=3", "n=4"]); }); }); describe("five trigger kinds", () => { test("http · every · signal · table.changed · internal", async () => { const seen: string[] = []; const httpFlow = on( http.get("/:code"), flow("t.http", { do: ({ code }: { code: string }) => { seen.push(`http:${code}`); return { code }; }, }), ); const hourly = clock("hourly", { every: "1h" }); on( hourly, flow("t.every", { do: () => { seen.push("every"); }, }), ); on( { name: "link-clicked", delivery: "once" }, flow("t.signal", { do: ({ code }: { code: string }) => { seen.push(`signal:${code}`); }, }), ); const db = store.sql("main"); const orders = store.schema.table("orders", { id: field.text().primaryKey(), status: field.text(), }); on( db.table(orders).changed("status"), flow("t.cdc", { do: (p: { before: { status: string }; after: { status: string } }) => { seen.push(`cdc:${p.before.status}->${p.after.status}`); }, }), ); on( internal, flow("t.internal", { do: () => { seen.push("internal"); return { ok: true }; }, }), ); const app = oke({ autoBoot: false, name: "five" }); const res = await app.fetch(new Request("http://localhost/abc", { method: "GET" })); expect(res.status).toBe(200); expect(await res.json()).toEqual({ data: { code: "abc" }, error: null }); await app.dispatchEvery("hourly"); await app.dispatchSignal("link-clicked", { code: "sa" }); await app.dispatchCdc( "orders", { before: { status: "open" }, after: { status: "paid" }, }, "status", ); const internalResult = await app.call("t.internal"); expect(internalResult).toEqual({ ok: true }); expect(seen).toEqual(["http:abc", "every", "signal:sa", "cdc:open->paid", "internal"]); expect(httpFlow.triggers[0]?.kind).toBe("http"); }); });