Skip to Content
卡片 SDK用模拟宿主测试

用模拟宿主测试

@neurosquad/card-sdk/testing 提供一个内存宿主,它使用真实的协议,并按应用的顺序执行应用的检查——参数 schema、权限、可见性, 以及(如果你愿意)速率限制。存储、设置、智能体、端口、工具、网络和文件都是模拟的。你的卡片代码无需改动就能在它上面运行。

单元测试

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' 也包括可选权限;或一个 id 列表。
context覆盖连接时发送的上下文(visibility、i18n、workspace、instance……)。
agents工作区中的 AgentInfo[]。
screens、replies按智能体 id 指定 readScreen 和 lastReply 的返回值。
peers相连的卡片(PeerInfo,外加 retained 保留值和用于请求的 respond 函数)。
files工作区文件夹:{ 'path/to/file': 'text' or Uint8Array }。
fetch在宿主完成 URL 和权限检查后处理 net.fetch:(req) => ({ status?, headers?, body?, stream?, error? })。默认返回 NETWORK_ERROR。
confirm应答 ui.confirm、openLink 和发给危险模式智能体的提示词。默认同意。
requestPermissions应答 permissions.request。默认授予所有申请的权限。
hostProtocol假装是更旧或更新的应用(用于测试 PROTOCOL_MISMATCH)。
enforceRateLimits应用逐方法的限流。默认 false,让测试保持确定性。
usageusage.summary 返回的 UsageSummary。
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)、端口值和工具参数的 schema、 只接受来自上游的端口消息、由状态变化推导出的轮次事件、填入请求头的密钥——但它既不是浏览器沙箱,也不是应用的界面:

  • 你的页面没有被沙箱隔离,所以表单可以跳转、copy 处理函数有效、嵌套框架和文件 Worker 都能加载——请用 neurosquad-card dev 在应用中检查这些;
  • 应用的对话框、标题栏和概览图块不会被绘制——请改为读取 host.chrome;
  • 没有真实的智能体、终端或箭头——它们就是你传入的数据。