# Testing with the SDK

> @playcode/sdk/testing: test your app's AI with no network and no credits - a scripted model, recorded runs played back, and checks for your own store.
> Source: https://playcode.io/docs/developers/sdk-testing - last reviewed 2026-10-17.

`@playcode/sdk/testing` lets you test your app's AI with no network and no credits. Each helper returns a `fetch` for `new Playcode({ fetch })`. Your code runs unchanged, through the same SDK code a real call runs: the same checks, retries and credit counting.

## A scripted model

`fakeModel(replies)` answers each model call with the next reply of a script, in the model maker's real stream shape:

```ts
import { Playcode, tool } from '@playcode/sdk'
import { fakeModel } from '@playcode/sdk/testing'
import { expect, it } from 'vitest'
import { z } from 'zod'

it('looks the order up before it answers', async () => {
  const fake = fakeModel(
    [
      { toolCalls: [{ name: 'findOrder', input: { number: 'A-1042' } }] },
      { text: 'It shipped on Monday.' },
    ],
    { model: 'test-model' },
  )
  const playcode = new Playcode({ apiKey: 'pcsk_test', fetch: fake.fetch })
  const findOrder = tool({
    description: 'An order by its number.',
    input: z.object({ number: z.string() }),
    execute: () => ({ status: 'shipped', shippedOn: 'Monday' }),
  })

  const agent = playcode.ai.agent({
    model: 'test-model',
    tools: { findOrder },
  })
  const result = await agent.run('Where is A-1042?')

  expect(result.text).toBe('It shipped on Monday.')
  expect(result.toolCalls[0]?.name).toBe('findOrder')
  // fake.calls holds what each model call sent, with its run and its user.
  expect(fake.calls).toHaveLength(2)
})
```

- A reply is `text`, `toolCalls`, `usage`, or an `error`: a Playcode refusal by its `code`, or a model maker's error that the SDK retries into the next reply.
- `usage.credits` is priced from the fake's card. Change the card with `card`, and the wire with `api`.
- A call the script has no reply for is refused as `fake_script_exhausted`.

## Recorded runs

`record()` keeps a real run as a cassette, and `replay(cassette)` plays it back with no request to Playcode:

```ts
import { readFileSync, writeFileSync } from 'node:fs'
import { Playcode } from '@playcode/sdk'
import { record, replay } from '@playcode/sdk/testing'

// Once, with a real key: this run spends credits.
const tape = record()
await runYourAgent(new Playcode({ fetch: tape.fetch }))
writeFileSync('agent.cassette.json', JSON.stringify(tape.cassette()))

// In every test after: no network, no credits.
const cassette = JSON.parse(readFileSync('agent.cassette.json', 'utf8'))
const { fetch } = replay(cassette)
await runYourAgent(new Playcode({ apiKey: 'pcsk_test', fetch }))
```

A replay answers each request with the recording of the same body, so parallel runs replay too. A request the cassette does not hold is refused as `cassette_mismatch`, naming the field that differs: your prompt or tools changed, so record again if you meant it. A cassette holds no key, because request headers are never recorded. It holds every prompt, file and answer of the run, so record with test data, never a customer's.

## The store contract

Your own `AgentStore`, over your database, must keep the conditional writes that durable runs need. `agentStoreContract` checks it:

```ts
import { agentStoreContract } from '@playcode/sdk/testing'
import { describe, it } from 'vitest'

describe('my agent store', () => {
  for (const check of agentStoreContract(() => new MyStore(db))) {
    it(check.name, check.run)
  }
})
```

Each check uses new ids of its own, so the checks can share one database. A failure throws a `StoreContractError` that says what the store did instead.

## In a Cloud app

A new Cloud app's backend has these tests already. `booking-agent.test.ts` replays the example agent from its cassette and checks that no request left the process. The backend's end-to-end tests run the real app against `fakeModel` behind a local server, and its browser tests drive the `/chat` page. `pnpm ai:smoke` then makes one real call, to prove the key, the address and a model: it spends credits.
