Skip to content

Testing

What pnpm test runs in each package, how the engine runs real Workflows in workerd, and where to add a test.

pnpm test runs turbo run test, which runs vitest run in every package that has tests. The test task has no dependsOn, so a failing dependency never silently skips a package’s tests. CI runs it after lint, type-check and build (.github/workflows/ci.yml), each step running even when an earlier one failed.

Package Environment Config
packages/core Node packages/core/vitest.config.ts
packages/design Node packages/design/vitest.config.ts
apps/engine workerd, through @cloudflare/vitest-plugin apps/engine/vitest.config.ts
apps/web Node apps/web/vitest.config.ts

packages/core/tests/:

File Covers
graph.test.ts Every template validates; the planner’s paths (the cheap purchase request skipping approvals, a rejection reaching its end through a join, the dunning loop to its limit and its early exit, subscription periods with distinct step runs then continue-as-new, a failed retry stopping a subscription, isSettled); validateGraph (forward cycles, unknown outputs, disconnected nodes, a second trigger, back-edges from outside the body, node config)
address.test.ts checkAddress: clean addresses pass unchanged; whitespace, country aliases, region names and US, CA and GB postcodes are corrected; unknown fields are dropped; missing fields, unknown regions and countries, bad postcodes and free-text ship-tos are rejected with every issue listed
api.test.ts startFingerprint: property order is ignored, while version, vars, values and line order change it
lines.test.ts stockLines: no list restocks every line merged per SKU; listed lines are capped at the ordered quantity; unknown SKUs, bad quantities and non-lists are ignored
roles.test.ts atLeast orders viewer < approver < operator; a principal round-trips through the proxy header, non-ASCII names included; a missing, malformed or unknown-role header is rejected
pricing.test.ts resolvePrice: list-price fallback; a stopping source ends the chain and an open one lets the lowest candidate win; priority order (price book before contract, and reordered); the deepest volume tier; every scope (SKU, tier, window, a rule with no price); sources left out of the chain
policy.test.ts evaluatePolicy: every rule, all failures reported together, quantities summed per SKU, business hours in a time zone, PO boxes in string and structured addresses, malformed rules rejected at validation
webhooks.test.ts signWebhook / verifyWebhook: a valid signature, a tampered body, a wrong secret, a missing header, a replay outside the tolerance

Planner semantics live only in packages/core/src/graph.ts, so a planner change comes with a test in graph.test.ts. Its simulate(graph, choose) helper drives the planner the way the Workflow does: it loops readyNodes → beginNode → complete, calling choose(id, run) to pick each node’s branches (or a loop’s next), and returns { ran, state, ended }, where ran lists node ids in the order they ran.

it('skips the approval for a small order', () => {
const { ran, ended } = simulate(tpl('purchase_request'), (id) =>
id === 'needs_mgr' ? ['false'] : undefined,
);
expect(ran).not.toContain('mgr');
expect(ended).toBe('done');
});

undefined from choose takes the node’s first output (and, for a loop, continues while run <= maxIterations). Check node ids against packages/core/src/templates.ts before writing them into a test.

The engine suite runs the real OrderAgent Durable Object and OrderLoop and Replenish Workflows against local D1 and R2, from the engine’s own wrangler.jsonc:

  • remoteBindings: false and the binding AI_MODE: 'mock', so every AI node and the copilot use their heuristics. No network or Cloudflare account is needed.
  • TIME_SCALE: '1'; tests skip waits with introspectWorkflow(env.ORDER_LOOP) and modifyAll(m => { m.disableSleeps(); m.disableRetryDelays(); m.forceEventTimeout({ name }) }), forcing named waitForEvent steps (by step key, for example mgr#1) to time out.
  • tests/setup.ts applies packages/db/migrations (read with readD1Migrations) to the test D1.
  • The plugin list includes agents/vite so the @callable() TC39 decorators are transformed.
  • testTimeout is 60 seconds.
File Covers
tests/agent.test.ts OrderAgent.report: replayed snapshots ignored, timeline keys deduped, a new generation accepted; terminal statuses and rolled-back nodes kept once set
tests/docs-sample-run.test.ts The B2B run the docs home page draws (apps/docs/src/data/sample-run.json) is still the run the engine produces: same nodes, statuses, branches, summaries and totals
tests/orders.test.ts End-to-end orders on every template: B2B extraction, auto-approval, shipping and payment; credit-limit approval and decline; chat proposals that don’t act; the dunning loop, with a timed node_runs row per run of each step and wait; out-of-stock failure and rollback; cancel releasing everything; subscription retries; reorder forecast and restock; storefront ship, capture, return and partial refund; restocking only a partial return’s lines; per-order reservations and the stock ledger under retries; policy cancel; return window closing; idempotent capped refunds; Idempotency-Key races, half-started replays and fingerprint mismatches; outbound webhook signing, retry and give-up; order.cancelled and order.failed; address validation branching on a published test flow; the pricing chain against the seeded sources (price book, contract, volume and list per customer, per-node switches, given prices, reordering and turning off sources); pricing in EUR at the stored rate, given prices left alone and an unknown currency rejected; credit checked in USD for a GBP order; the agent socket refusing a viewer’s decide and cancel and a socket without a principal, and recording an approver’s own email as by; an open socket following a role change, a disabled user, a signed-out session and a forged session id; the offline copilot

Tests drive orders through startOrder, then poll the agent’s snapshot() until a predicate holds (until, waitingOn, settled), and assert on D1 rows (payments, reservations, shipments, approvals, webhook_deliveries) as well as state.

packages/design/tests/tokens.test.ts holds the Ledger tokens in packages/design/src/tokens.ts to apps/web/app/app.css, value for value in both schemes, plus the font stacks, and fails when the generated apps/docs/src/styles/theme.css is stale. Fix it by mirroring the app.css change in tokens.ts and running pnpm sync:theme.

apps/web/tests/:

File Covers
openapi.test.ts The OpenAPI document serialises, request bodies come from the zod schemas, every outbound event is listed, signed inbound events are unauthenticated; subscription URLs require https except on localhost and default to every event
credentials.test.ts PBKDF2 hashes verify only the right password and use a fresh salt; malformed or out-of-range hashes fail; ol_live_ tokens are stored as SHA-256 with a 12-character prefix; generated passwords, email normalisation, safeEqual; the password policy
throttle.test.ts Sign-in keys (email and address, address, email; the workspace password as one email); a key blocks at its limit until its window ends, old windows don’t count, each kind has its own limit and the longest wait wins; waits round up to whole minutes
origin.test.ts crossOrigin: reads pass from anywhere; writes from another origin, a sibling subdomain, null or plain http are refused; the WebSocket handshake is checked although it is a GET; the Sec-Fetch-Site fallback; clients with neither header pass
audit.test.ts Audit events described in one line (role changes as from → to, token roles, failure reasons); every action belongs to a filter group
tokens.test.ts API token lifetimes: 30 days, 1 year and never; token state active, expiring within 14 days, expired at its expiry time, revoked first
approvals.test.ts Approval context for a credit check: the order in its own currency with its USD equivalent, and limits, balances and the shortfall in USD
proposal.test.ts The chat proposal card offers only the proposed decision, prefilled, sends nothing on render, and has nothing to confirm once the step stopped waiting or the order ended
scenarios.test.ts Every demo scenario targets a valid seeded flow of its order type; money, relative time, UTC date and duration formatting; status-to-token mapping

The flow editor canvas and the live order page are not under test; verify UI changes by running pnpm web:dev and driving a scenario. See Getting started.