Skip to content

Testing model calls

An activity reaches a model only from an action, so what a test scripts is the poe.modelProxy.stream platform capability — not an HTTP response. Pass a createPlatformCaller to createPoeTileTestHarness that answers it with SSE frames in the shape the model codec emits.

typescript
import { test, expect } from "bun:test";
import {
  createMockPlatformCaller,
  createPoeTileTestHarness,
} from "poe-tiles-sdk/v1/test-utils.js";
import { myBackendConfig } from "../synced-store/backend-config";

function frame(event: Record<string, unknown>): string {
  return `event: ${String(event["type"])}\ndata: ${JSON.stringify(event)}\n\n`;
}

/** A finished single-chunk round carrying `text`. */
function modelReply(text: string): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  return new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(encoder.encode(frame({ type: "text", delta: text })));
      controller.enqueue(
        encoder.encode(frame({ type: "finish", finishReason: "stop" })),
      );
      controller.close();
    },
  });
}

test("summarize returns the model's reply", async () => {
  const harness = createPoeTileTestHarness({
    store: { backendConfig: myBackendConfig },
    createPlatformCaller: () =>
      createMockPlatformCaller({
        "poe.modelProxy.stream": async () => modelReply("Hello World!"),
      }),
  });
  const { store } = await harness.createClient();

  const { text } = await store.action.summarize({ prompt: "Hi" });
  expect(text).toBe("Hello World!");

  harness.dispose();
});

Event types

FrameMeaning
{ type: "text", delta }One chunk of reply text
{ type: "finish", finishReason }The round ended normally
{ type: "error", code, message, retryable }The round failed upstream — createModelStream throws ModelStreamError

Emit several text frames to test that your action accumulates chunks correctly, and an error frame to test the failure path.

Assert what LEFT the activity, not just what came back

Two failures here are silent, so record the dispatched input:

  • The model id. A model id the catalog does not stock fails every call — and if your action has a fallback, the game plays on serving the fallback forever with nothing surfaced.
  • The credential. An activity must never forward one. Check the exact key set rather than the absence of poeApiKey, so an invented credential field is caught too.
typescript
const rounds: Array<{ model: string; keys: string[] }> = [];
createMockPlatformCaller({
  "poe.modelProxy.stream": async (input) => {
    rounds.push({ model: input.model, keys: Object.keys(input).sort() });
    return modelReply("ok");
  },
});
// ...
expect(rounds[0].model).toBe("anthropic/claude-opus-5/anthropic");
expect(rounds[0].keys).toEqual(["model", "request"]);

Images

Image generation is the non-streaming sibling: script "poe.modelProxy.generateHostedImage" and return { imageUrl }. It resolves a served URL rather than bytes, so the value your test hands back is exactly what a store row will hold.

typescript
createMockPlatformCaller({
  "poe.modelProxy.generateHostedImage": async () => ({
    imageUrl: "https://tiles.test/blobs/gen/abc.png",
  }),
});