# Testing with the mock host

> createMockHost — an in-memory NeuroSquad that applies the app's checks, records what your card did, and lets you drive it from tests or a browser preview.

Source: https://docs.neurosquad.ai/en/card-sdk/testing

`@neurosquad/card-sdk/testing` has an in-memory host that speaks the real protocol and applies the
app's checks in the app's order — params schemas, permissions, visibility and, if you want, rate
limits. Storage, settings, agents, ports, tools, network and files are simulated. Your card code
runs unchanged against it.

## A unit test

```ts
import { createMockHost } from '@neurosquad/card-sdk/testing'
import type { CardManifest } from '@neurosquad/card-sdk'
import { describe, expect, it } from 'vitest'

const manifest: CardManifest = {
  manifestVersion: 1,
  name: 'test-radar',
  displayName: 'Test radar',
  version: '1.0.0',
  protocol: 1,
  permissions: ['agents.read'],
  ports: { outputs: [{ id: 'summary', label: 'Summary', type: 'ns:markdown' }] },
  tools: [{ name: 'summary', description: 'Returns the summary.', inputSchema: { type: 'object' } }]
}

describe('test radar', () => {
  it('sends its summary to a connected note and answers the tool', async () => {
    const host = createMockHost({
      manifest,
      agents: [{ id: 'a1', name: 'Claude Code', harness: 'claude-code', kind: 'ai', status: 'idle', connected: true }],
      peers: [
        {
          cardId: 'note-1',
          kind: 'note',
          name: 'Notes',
          direction: 'downstream',
          inputs: [{ id: 'append', label: 'Append', type: 'ns:markdown', mode: 'stream', retain: false, default: true }],
          outputs: []
        }
      ]
    })
    const card = await host.connect()

    // The code under test would normally live in your card's module.
    card.tools.handle('summary', () => '# All green')
    await card.ports.emit('summary', '# All green')

    expect(host.deliveries).toEqual([{ to: 'note-1', output: 'summary', input: 'append', data: '# All green' }])
    await expect(host.callTool('summary', {})).resolves.toEqual({
      content: [{ type: 'text', text: '# All green' }]
    })
  })

  it('is refused what it did not declare', async () => {
    const card = await createMockHost({ manifest }).connect()
    await expect(card.agents.prompt('a1', 'hi')).rejects.toMatchObject({ code: 'PERMISSION_DENIED' })
  })
})
```

The mock needs `MessageChannel` — Node 18+ and every browser have it.

## Options

`createMockHost(options)`:

| Option | Meaning |
| --- | --- |
| `manifest` | Your manifest, validated like the app does (an invalid one throws). Default: a minimal one. |
| `grant` | Granted permissions: default every **required** one; `'all'` includes optional ones; or a list of ids. |
| `context` | Overrides of the context sent at connect (`visibility`, `i18n`, `workspace`, `instance`…). |
| `agents` | `AgentInfo[]` in the workspace. |
| `screens`, `replies` | What `readScreen` and `lastReply` return, per agent id. |
| `peers` | Connected cards (`PeerInfo`, plus `retained` values and a `respond` function for requests). |
| `files` | The workspace folder: `{ 'path/to/file': 'text' or Uint8Array }`. |
| `fetch` | Handles `net.fetch` after the host's URL and permission checks: `(req) => ({ status?, headers?, body?, stream?, error? })`. Default: `NETWORK_ERROR`. |
| `confirm` | Answers `ui.confirm`, `openLink` and dangerous-mode prompts. Default: yes. |
| `requestPermissions` | Answers `permissions.request`. Default: grants everything asked. |
| `hostProtocol` | Pretend to be an older or newer app (to test `PROTOCOL_MISMATCH`). |
| `enforceRateLimits` | Apply per-method throttles. Default `false`, so tests stay deterministic. |
| `usage` | The `UsageSummary` returned by `usage.summary`. |
| `settings` | Initial setting values, as if the user had saved them. |
| `secrets` | Values of secret settings the user has entered: filled into `{{secret:key}}` headers; a declared secret missing here fails with `INVALID_PARAMS`, as in the app. |
| `verbose` | Log every request and event. |

## Driving the card

| Method | Does |
| --- | --- |
| `connect(options?)` | A real `Card` wired to this host. |
| `setVisibility(state)`, `setExpanded(bool)`, `resize({ w, h })`, `suspend(graceMs?)` | Lifecycle events. |
| `setTheme(theme)`, `setLanguage('ru')` | Theme and language switches. |
| `setSettings(values, secrets?)` | The user saved the settings form (`secrets`: which ones are set). |
| `setSecret(key, value)` | The user entered (or, with `null`, cleared) a secret setting. |
| `setGrants(ids)`, `setPeers(peers)` | Change grants or arrows (sends the matching events). |
| `sendPortMessage(input, data, from?)` | A value arrives on an input. |
| `requestPort(input, data)` | Ask a request input; resolves with the card's answer. |
| `callTool(tool, args, { agent?, timeoutMs? })` | Call a tool like an agent; resolves with the result. `cancelTool(callId)` cancels. |
| `setAgentStatus(id, status)`, `agentOutput(id, text)` | Agent events (delivered if the card subscribed); a status change also produces `agents.turn` by the app's rule. |
| `touchFile(path, data?)`, `readFile(path)` | Change a file (notifying watchers), read what the card wrote. |
| `emit(event, data)` | Send any event. |
| `ping()` | A heartbeat; resolves when the card answers. |
| `handle(method, fn)` | Replace a method's behaviour (runs after the checks). |

## What it records

`calls` (every request), `events`, `storage.instance` / `storage.package` (Maps), `deliveries`,
`retained`, `prompts`, `terminal`, `logs`, `toasts`, `clipboard`, `spawned`, `aborted`, `pongs`,
`subscriptions`, `chrome` (`title`, `status`, `badge`, `overview`, `attention`, `menu`,
`enabledTools`), `files`, and the current `hostContext` and `grants`.

## Previews in a browser

Both templates start the mock host when the page is opened outside the app, so `npx serve .` or
`npm run dev` shows a working card with sample data. The pattern, if you set it up yourself:

```ts
import { connect, type Card, type CardManifest } from '@neurosquad/card-sdk'

async function start(): Promise<Card> {
  if (window.parent !== window) return connect()          // inside NeuroSquad
  const { createMockHost } = await import('@neurosquad/card-sdk/testing')
  const manifest = (await (await fetch('./neurosquad-card.json')).json()) as CardManifest
  const host = createMockHost({ manifest, grant: 'all' })
  Object.assign(window, { mockHost: host })               // drive it from the DevTools console
  return host.connect()
}

const app = await start()
render(app.instance.displayName)
```

> The mock is faithful about **rules**, not about **the rest of the app**: it does not run agents,
> draw arrows or render your overview tile. Before you publish, run the card for real with
> `neurosquad-card dev`.

## What the mock does not simulate

The mock mirrors the app's rules — permissions, visibility (`NOT_VISIBLE` for dialogs and
permission requests), schemas on port values and tool arguments, upstream-only port messages, turn
events derived from status changes, secrets filled into headers — but it is not a browser sandbox
and not the app's UI:

- your page is not sandboxed, so forms can navigate, `copy` handlers work, nested frames and file
workers load — check those in the app with `neurosquad-card dev`;
- the app's dialogs, header and overview tile are not drawn — read `host.chrome` instead;
- no real agents, terminals or arrows exist — they are the data you pass in.
