Тесты с мок-хостом
В @neurosquad/card-sdk/testing есть хост в памяти, который говорит на настоящем протоколе и
применяет проверки приложения в том же порядке — схемы параметров, разрешения, видимость и, по
желанию, ограничения частоты. Хранилище, настройки, агенты, порты, инструменты, сеть и файлы
имитируются. Код вашей карточки работает с ним без изменений.
Модульный тест
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. |
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 показывают рабочую карточку с примерами данных. Если настраиваете сами, схема такая:
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; - настоящих агентов, терминалов и стрелок нет — это данные, которые вы передаёте.