Skip to main content

Setup

Import testing utilities from @jelou/functions/testing:
import {
  createMockContext,
  createMockCronContext,
  createMockEventContext,
  createMockRequest,
  createMockPlatformRequest,
  createMockJelouClient,
  createMockMemoryClient,
} from "@jelou/functions/testing";

createMockContext(overrides?)

Creates a context with sensible defaults for testing.
const ctx = createMockContext();

const ctx2 = createMockContext({
  company: { id: 42, name: "Shop ABC" },
  user: { id: 99, names: "Maria Garcia" },
  bot: { id: "bot-123", name: "Support Bot", channel: "whatsapp" },
  env: {
    get: (key) => key === "CRM_API_KEY" ? "sk-test-123" : undefined,
    has: (key) => key === "CRM_API_KEY",
    toObject: () => ({ CRM_API_KEY: "sk-test-123" }),
  },
});

createMockCronContext(expression, overrides?, cronName?)

Creates a context with isCron: true and trigger.type: "cron".
const ctx = createMockCronContext("0 9 * * *");
// ctx.isCron === true
// ctx.trigger === { type: "cron", cron: "0 9 * * *" }
// ctx.isHttp === false

const ctx2 = createMockCronContext("0 9 * * *", {}, "daily-reminder");
// ctx2.trigger === { type: "cron", cron: "0 9 * * *", cronName: "daily-reminder" }

createMockEventContext(eventName, overrides?)

Creates a context with isEvent: true and trigger.type: "event".
const ctx = createMockEventContext("payment.completed");
// ctx.isEvent === true
// ctx.trigger === { type: "event", event: "payment.completed" }

createMockRequest(body?, options?)

Creates a standard Web Request object.
const req = createMockRequest();
// GET http://localhost:8000/

const req2 = createMockRequest({ phone: "593987654321" });
// POST with JSON body, Content-Type: application/json

const req3 = createMockRequest(null, {
  method: "PUT",
  url: "https://my-function.fn.jelou.ai/clients/42",
  headers: { "x-api-key": "sk-test-123" },
});

createMockPlatformRequest(token, body?, options?)

Creates a Request with the X-Jelou-Token header. Useful for testing functions that receive requests authenticated with runtime tokens.
const req = createMockPlatformRequest("jfn_rt_test123");
// GET with X-Jelou-Token: jfn_rt_test123

const req2 = createMockPlatformRequest("jfn_rt_test123", { query: "test" });
// POST with JSON body + X-Jelou-Token: jfn_rt_test123

const req3 = createMockPlatformRequest("jfn_rt_test123", null, {
  method: "PUT",
  url: "https://my-function.fn.jelou.ai/admin",
  headers: { "x-custom": "value" },
});
// PUT with X-Jelou-Token + additional headers

createMockJelouClient(options?)

Creates a mock messaging client with call recording.
const mockJelou = createMockJelouClient();
const ctx = createMockContext({ jelou: mockJelou });

await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Hello" });

// Inspect calls
mockJelou.calls.length;          // 1
mockJelou.calls[0].method;       // "send"
mockJelou.calls[0].args.type;    // "text"
mockJelou.calls[0].timestamp;    // Date.now()

Custom results

const mockJelou = createMockJelouClient({
  sendResult: { messageId: "msg-custom-123" },
  templateResult: [{ id: "tmpl-1", destination: "+593987654321" }],
});

Reset between tests

mockJelou.reset(); // clears recorded calls

createMockMemoryClient(options?)

Creates a mock memory client with an in-memory store.
const mockMemory = createMockMemoryClient({
  store: { step: "start", attempts: 0 },
});
const ctx = createMockContext({ memory: mockMemory });

const step = await ctx.memory.get("step", "unknown"); // "start"
await ctx.memory.set("step", "confirm", 3600);

Inspect calls

mockMemory.calls.length;         // 2 (get + set)
mockMemory.calls[1].method;      // "set"
mockMemory.calls[1].args;        // { key: "step", value: "confirm", ttl: 3600 }

Reset between tests

mockMemory.reset(); // clears calls and resets store

Full example

index.test.ts
import { assertEquals } from "jsr:@std/assert";
import { define, z } from "@jelou/functions";
import {
  createMockContext,
  createMockCronContext,
  createMockRequest,
  createMockPlatformRequest,
  createMockJelouClient,
  createMockMemoryClient,
} from "@jelou/functions/testing";

const fn = define({
  name: "lookup-customer",
  description: "Looks up customer info by phone",
  input: z.object({
    phone: z.string().min(10),
  }),
  output: z.object({
    name: z.string(),
    plan: z.string(),
    companyId: z.number(),
  }),
  handler: async (input, ctx) => {
    return {
      name: "Maria Garcia",
      plan: "Premium",
      companyId: ctx.company.id,
    };
  },
});

Deno.test("returns customer info with correct company ID", async () => {
  const ctx = createMockContext({
    company: { id: 42, name: "Shop ABC" },
  });
  const req = createMockRequest({ phone: "593987654321" });

  const result = await fn.handler({ phone: "593987654321" }, ctx, req);

  assertEquals(result.name, "Maria Garcia");
  assertEquals(result.companyId, 42);
});

Deno.test("cron skip when not a cron trigger", async () => {
  const cronFn = define({
    name: "daily-cleanup",
    input: z.object({}),
    config: { cron: [{ expression: "0 3 * * *" }] },
    handler: async (_input, ctx) => {
      if (!ctx.isCron) return { skipped: true };
      return { cleaned: true };
    },
  });

  const httpCtx = createMockContext();
  const req = createMockRequest();
  assertEquals(await cronFn.handler({}, httpCtx, req), { skipped: true });

  const cronCtx = createMockCronContext("0 3 * * *");
  assertEquals(await cronFn.handler({}, cronCtx, req), { cleaned: true });
});

Deno.test("messaging mock records calls", async () => {
  const mockJelou = createMockJelouClient();
  const ctx = createMockContext({ jelou: mockJelou });

  await ctx.jelou.send({
    type: "text",
    to: "+593987654321",
    text: "Your order is ready",
  });

  assertEquals(mockJelou.calls.length, 1);
  assertEquals(mockJelou.calls[0].method, "send");
  assertEquals(mockJelou.calls[0].args.type, "text");
});

Deno.test("memory mock persists values", async () => {
  const mockMemory = createMockMemoryClient({ store: { step: "start" } });
  const ctx = createMockContext({ memory: mockMemory });

  assertEquals(await ctx.memory.get("step", "unknown"), "start");
  await ctx.memory.set("step", "confirm", 3600);
  assertEquals(await ctx.memory.get("step", "unknown"), "confirm");
});

Deno.test("platform request includes token header", () => {
  const req = createMockPlatformRequest("jfn_rt_test123", { action: "test" });
  assertEquals(req.headers.get("x-jelou-token"), "jfn_rt_test123");
  assertEquals(req.method, "POST");
});

createMockApp(tools, config?)

Creates a mock EdgeApp for testing multi-tool functions.
import { assertEquals } from "jsr:@std/assert";
import { createMockApp, createMockContext, createMockRequest } from "@jelou/functions/testing";
import { define, z } from "@jelou/functions";

const myApp = createMockApp({
  sendEmail: define({
    description: "Sends an email",
    input: z.object({ to: z.string() }),
    handler: async (input) => ({ sent: true }),
  }),
  readInbox: define({
    description: "Reads inbox messages",
    input: z.object({}),
    handler: async () => ({ messages: [] }),
  }),
});

Deno.test("app has correct tools", () => {
  assertEquals(Object.keys(myApp.tools), ["sendEmail", "readInbox"]);
  assertEquals(myApp.__jelou_edge_app, true);
});

Deno.test("each tool works independently", async () => {
  const ctx = createMockContext();
  const req = createMockRequest({ to: "[email protected]" });
  const result = await myApp.tools.sendEmail.handler(
    { to: "[email protected]" },
    ctx,
    req,
  );
  assertEquals(result, { sent: true });
});
Run tests with Deno:
deno test