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 |
Core: the planner and pure logic
Section titled “Core: the planner and pure logic”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 |
Adding a planner test
Section titled “Adding a planner test”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.
Engine: real Workflows in workerd
Section titled “Engine: real Workflows in workerd”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: falseand the bindingAI_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 withintrospectWorkflow(env.ORDER_LOOP)andmodifyAll(m => { m.disableSleeps(); m.disableRetryDelays(); m.forceEventTimeout({ name }) }), forcing namedwaitForEventsteps (by step key, for examplemgr#1) to time out.tests/setup.tsappliespackages/db/migrations(read withreadD1Migrations) to the test D1.- The plugin list includes
agents/viteso the@callable()TC39 decorators are transformed. testTimeoutis 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.
Design
Section titled “Design”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 |
What isn’t covered
Section titled “What isn’t covered”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.