# Тесты с мок-хостом

> createMockHost — NeuroSquad в памяти, который применяет проверки приложения, записывает, что сделала ваша карточка, и позволяет управлять ею из тестов или превью в браузере.

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

В `@neurosquad/card-sdk/testing` есть хост в памяти, который говорит на настоящем протоколе и
применяет проверки приложения в том же порядке — схемы параметров, разрешения, видимость и, по
желанию, ограничения частоты. Хранилище, настройки, агенты, порты, инструменты, сеть и файлы
имитируются. Код вашей карточки работает с ним без изменений.

## Модульный тест

```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' })
  })
})
```

Моку нужен `MessageChannel` — он есть в Node 18+ и в любом браузере.

## Параметры

`createMockHost(options)`:

| Параметр | Значение |
| --- | --- |
| `manifest` | Ваш манифест; проверяется так же, как в приложении (неверный — исключение). По умолчанию — минимальный. |
| `grant` | Выданные разрешения: по умолчанию все **обязательные**; `'all'` — вместе с необязательными; или список идентификаторов. |
| `context` | Переопределения контекста, отправляемого при подключении (`visibility`, `i18n`, `workspace`, `instance`…). |
| `agents` | `AgentInfo[]` в воркспейсе. |
| `screens`, `replies` | Что возвращают `readScreen` и `lastReply` для каждого агента. |
| `peers` | Подключённые карточки (`PeerInfo`, плюс сохраняемые значения `retained` и функция `respond` для запросов). |
| `files` | Папка воркспейса: `{ 'path/to/file': 'text' or Uint8Array }`. |
| `fetch` | Обрабатывает `net.fetch` после проверок URL и разрешений: `(req) => ({ status?, headers?, body?, stream?, error? })`. По умолчанию — `NETWORK_ERROR`. |
| `confirm` | Отвечает на `ui.confirm`, `openLink` и промпты в опасном режиме. По умолчанию — «да». |
| `requestPermissions` | Отвечает на `permissions.request`. По умолчанию выдаёт всё, что просят. |
| `hostProtocol` | Притвориться более старым или новым приложением (чтобы проверить `PROTOCOL_MISMATCH`). |
| `enforceRateLimits` | Применять ограничения частоты методов. По умолчанию `false`, чтобы тесты были детерминированными. |
| `usage` | `UsageSummary`, который возвращает `usage.summary`. |
| `agentUsage` | Что возвращает `agents.usage` по id агента (поверх нулевого прогона) или функция от `(agentId, since, until)`. Поменять позже — `setAgentUsage`. |
| `settings` | Начальные значения настроек, как будто пользователь их сохранил. |
| `secrets` | Значения секретных настроек, которые ввёл пользователь: подставляются в заголовки `{{secret:key}}`; объявленный секрет, которого здесь нет, падает с `INVALID_PARAMS`, как в приложении. |
| `verbose` | Печатать каждый запрос и событие. |

## Управление карточкой

| Метод | Делает |
| --- | --- |
| `connect(options?)` | Настоящий `Card`, подключённый к этому хосту. |
| `setVisibility(state)`, `setExpanded(bool)`, `resize({ w, h })`, `suspend(graceMs?)` | События жизненного цикла. |
| `setTheme(theme)`, `setLanguage('ru')` | Смена темы и языка. |
| `setSettings(values, secrets?)` | Пользователь сохранил форму настроек (`secrets` — какие заданы). |
| `setSecret(key, value)` | Пользователь ввёл (или, с `null`, очистил) секретную настройку. |
| `setGrants(ids)`, `setPeers(peers)` | Меняет выдачу разрешений или стрелки (с соответствующими событиями). |
| `sendPortMessage(input, data, from?)` | На вход приходит значение. |
| `requestPort(input, data)` | Обращается к входу-запросу; возвращает ответ карточки. |
| `callTool(tool, args, { agent?, timeoutMs? })` | Вызывает инструмент, как агент; возвращает результат. `cancelTool(callId)` отменяет. |
| `setAgentStatus(id, status)`, `agentOutput(id, text)` | События агентов (доставляются, если карточка подписана); смена статуса порождает и `agents.turn` по правилу приложения. |
| `touchFile(path, data?)`, `readFile(path)` | Меняет файл (уведомляя наблюдателей), читает то, что записала карточка. |
| `emit(event, data)` | Отправляет любое событие. |
| `ping()` | Пульс; разрешается, когда карточка ответит. |
| `handle(method, fn)` | Заменяет поведение метода (выполняется после проверок). |

## Что он записывает

`calls` (каждый запрос), `events`, `storage.instance` / `storage.package` (Map), `deliveries`,
`retained`, `prompts`, `terminal`, `logs`, `toasts`, `clipboard`, `spawned`, `aborted`, `pongs`,
`subscriptions`, `chrome` (`title`, `status`, `badge`, `overview`, `attention`, `menu`,
`enabledTools`), `files`, а также текущие `hostContext` и `grants`.

## Превью в браузере

Оба шаблона запускают мок-хост, если страницу открыть вне приложения, так что `npx serve .` или
`npm run dev` показывают рабочую карточку с примерами данных. Если настраиваете сами, схема такая:

```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)
```

> Мок точен в **правилах**, но не в **остальном приложении**: он не запускает агентов, не рисует
> стрелки и не показывает вашу плитку обзора. Перед публикацией запустите карточку по-настоящему через
> `neurosquad-card dev`.

## Чего мок не имитирует

Мок повторяет правила приложения — разрешения, видимость (`NOT_VISIBLE` для диалогов и запросов
разрешений), схемы значений портов и аргументов инструментов, сообщения портов только от карточек
выше по стрелке, события хода, выведенные из смены статуса, подстановку секретов в заголовки, — но
он не песочница браузера и не интерфейс приложения:

- ваша страница не в песочнице, поэтому формы могут уводить страницу, обработчики `copy` работают,
вложенные фреймы и воркеры из файлов загружаются — проверяйте это в приложении через
`neurosquad-card dev`;
- диалоги, шапка и плитка обзора приложения не рисуются — смотрите `host.chrome`;
- настоящих агентов, терминалов и стрелок нет — это данные, которые вы передаёте.
