@syllm/brickly-test-host
v0.14.0
Published
Brickly 真宿主测试工件:HostGrpcServer + 控制面(故障注入/请求录制/host→runtime 驱动面),供各端 SDK 契约测试使用
Readme
@syllm/brickly-test-host
Brickly 真宿主测试工件:把生产级 HostGrpcServer(含真 BrickStorageService 内存版)+ 测试控制面打成零依赖单文件 bundle,供各端 SDK 的契约 / conformance 测试使用。
- 协议:
brickly.runtime.v1,版本号与 SDK 发布线对齐(0.11.0 起) - 定位:替代各端手写 FakeHost 的"演好人"职责;SDK 单测仍需假宿主做低层故障注入时,可继续用本包的控制面故障注入(
SetNextError语义)
交付物
npm i -D @syllm/brickly-test-host 后得到:
| 文件 | 用途 |
|------|------|
| dist/host.cjs | 独立宿主进程入口(bin:brickly-test-host),零 npm 依赖,Node ≥18 即可运行 |
| dist/index.cjs | Node 库入口:startTestHost(进程内)/ spawnTestHost(子进程) |
| dist/sources.lst | bundle 输入文件清单(可追溯打进来的源码) |
跨语言用法(Go / .NET / Python 等)
1. spawn 协议
node node_modules/@syllm/brickly-test-host/dist/host.cjs
# 或安装后:npx brickly-test-hoststdout 首行输出就绪 JSON:
{"dataEndpoint":"127.0.0.1:50051","controlEndpoint":"127.0.0.1:50052","runtimeToHostToken":"..."}关闭:stdin 结束 或 SIGINT/SIGTERM → 宿主优雅退出
dataEndpoint是 gRPC 数据面;runtimeToHostToken是 SDK 连接宿主要带的运行时凭据
2. 控制面 HTTP(仅 127.0.0.1)
故障注入与录制(runtime→host 方向)
| 方法 | 路径 | 请求体 | 说明 |
|------|------|--------|------|
| POST | /faults | {path, brickCode?, action?, ms?, match?, sequence?, count?} | 故障注入(剧本面):命中 path 的下 count 次调用(默认 1)按规则作恶,无副作用(真 handler 不执行) |
| POST | /reset | {} | 清空故障规则、调用录制、UI/搜索罐头响应与各自调用记录(不清 storage 数据) |
| GET | /calls | — | {"calls":[{"path","at","invocationId"?,"intent"?,"request"?,"frames"}]} 调用录制:invocationId/intent 取自 x-brickly-invocation-id/x-brickly-intent 头;request 为首个入站请求消息体(bytes 字段归一化为 {$bytes:"<base64>"});frames 为入站消息总数(client-streaming 分帧计数用) |
path 为完整 gRPC 方法路径,如 /brickly.runtime.v1.BrickStorageService/KvGet;
brickCode 为协议错误码,如 BRICK_STORAGE_NOT_FOUND、LIMIT_EXCEEDED。
/faults 规则形态(三选一):
- error(旧形态兼容):
{path, brickCode, message?, count?}——短路返回该 brick 错误; - hang:
{path, action:"hang"}——调用永不应答,挂到客户端 deadline; - delay:
{path, action:"delay", ms}——处理与响应整体推后 ms; - sequence:
{path, sequence:[{action:"error",brickCode},{action:"delay",ms},{action:"hang"}]}——按调用次序逐条消费,耗尽后放行; - match(可叠加于以上任一):
{path, match:{...}}——对首个入站请求体做深部分匹配(递归字段子集),命中才消费规则作恶,未命中直接放行且不消耗次数;match+delay 时延迟窗口内的后续消息与 half-close 被缓冲、延迟结束后按原序释放。
驱动面(host→runtime 方向)
| 方法 | 路径 | 请求体 | 响应 |
|------|------|--------|------|
| POST | /spawn | {brickId, instanceId?, origin?, version?} | 完整 spawn 凭据 {spawnId, bootstrapToken, runtimeToHostToken, hostToRuntimeToken, brickId, instanceId}——铸给第二个 runtime 用(依赖调用等场景) |
| POST | /invoke | {spawnId?, commandId, input?, timeoutMs?, invocationId?} | {ok:true,invocationId,result} 或 {ok:false,invocationId,error:{brickCode?,message,grpcStatus?}};缺省 spawnId 用首个 spawn,invocationId 缺省自动生成(驱动 command scope 语义) |
| POST | /interact | 同 /invoke | 批量模式:{ok:true,invocationId,events:[...],result} 或 {ok:false,...} |
| POST | /interact-script | {spawnId?, commandId?, input?, open?, invocationId?, timeoutMs?, expectTimeoutMs?, steps:[...]} | 裸帧剧本:{ok,state,received};语义违规帧注入(首帧非 open、sequence 跳号、坏 messageId、中途 cancel_request、挂死) |
| POST | /events/push | {spawnId?, topic, payload?, delayMs?} | {},经真 HostEventBus 送达该 runtime 的事件订阅流;delayMs 延迟投递 |
| POST | /ui-handler | {method, result} | 注册 UI 方法罐头响应;method 限 createBrowserWindow / callWindow / closeWindow / forceCloseWindow / listWindows / sendToChild / replyToChild / emitToChild,未知名返回 400 |
| POST | /platform-handler | {method, result} | 注册任意 wire 平台方法罐头(clipboard.readContent/screen.*/input.*/system.*/runtime.invoke 等),重复注册覆盖;$interact 为 PlatformService.Interact 最终结果罐头(runtime.call/interact 意图用),$interact.request 为 request 应答罐头。安装后所有非 ui.window.* 平台调用都走本桩,未注册方法报 宿主未接入 <method> |
| GET | /ui-calls | — | {"calls":[{"method","args","at"}]} runtime 真实发起的 UI 调用记录;callWindow 的 args 为 [caller, instanceId, windowId, method, args, parentRequestId] |
| POST | /search-handler | {method, result} | 注册 search.* 平台方法罐头响应;method 限 query / activate / runAction,未知名返回 400;与 /platform-handler 共用同一罐头桩(两者装的是同一个 handler) |
| GET | /search-calls | — | {"calls":[{"method","input","callerBrickId","at"}]} runtime 真实发起的 search.* 调用记录 |
| GET | /runtimes | — | {"runtimes":[{"spawnId","brickId","instanceId"}]} 已注册 runtime 句柄 |
| POST | /kernel/dependency | {commands?: string[]} | 接通依赖调用内核:runtime 侧 dependencies.require(alias).start() / handle invoke 经真 ConnectorApplicationService 路由到已注册 runtime;catalog 按 spawn 动态合成——每个 brick 对其他 brick 通配声明 commands:['*'] 依赖,commands 参数声明目标 brick 暴露的命令面 |
典型流程:POST /spawn 拿凭据 → 测试进程按 BRICKLY_HOST_ENDPOINT / BRICKLY_BOOTSTRAP_TOKEN / BRICKLY_RUNTIME_TO_HOST_TOKEN / BRICKLY_HOST_TO_RUNTIME_TOKEN 环境变量起 SDK runtime → POST /invoke 反向调用命令 → POST /ui-handler + GET /ui-calls 验证 UI 调用链。
/interact-script 步进 DSL(host→runtime 裸 ClientFrame 剧本,替代 FakeInteractSession):
- 顶层
commandId/input且open缺省 true 时,开流自动发 open 帧(open:false则首帧由剧本自定——首帧违规用例); - 步形态:
{open:{commandId?,input?}}{event:<js>}{request:<js>,as:"别名"}{cancelRequest:"别名|hex"}{raw:{sequence?,messageId?,replyTo?,open?|event?|request?|cancelRequest?}}{halfClose:true}{cancel:true}{sleepMs:n}{hang:true}{expect:<深部分匹配>}{expectTimeoutMs:n}; - 出站
sequence自动从 1 递增,raw步可显式覆盖构造乱序;request自动生成 16-byte messageId 并可用as记别名; - 入站帧归一化为
{kind:'opened'|'event'|'response'|'final', sequence, replyTo?, event?, response?, final?};远端半关闭/流错误入队为{kind:'end'}/{kind:'error',error}伪帧; expect对下一入站帧做深部分匹配(与/faultsmatch 同规则),失配 →state:'script-error'并带failedStep;hang停止执行剩余步、保持流开,直到远端终止或timeoutMs到期(返回state:'hang');- 返回
{ok,state,received,error?,failedStep?}:state∈ended|error|cancelled|hang|open|timeout|script-error;received为全量入站录制(expect 消费不动它)。
示例——验证 runtime 对"首帧非 open"的拒绝:
POST /interact-script
{ "open": false,
"steps": [ {"event": {"n":1}}, {"expect": {"kind":"error"}} ] }
→ { "ok": true, "state": "error", "received": [{"kind":"error","error":"13 INTERNAL: PROTOCOL_VIOLATION: 首帧必须是 open 且 sequence=1"}] }依赖场景:先 POST /kernel/dependency 接通内核,再 /spawn 铸第二套凭据起依赖目标 runtime;调用方 runtime 需额外注入 BRICKLY_DEPENDENCY_BINDINGS(JSON:alias → {brickId, origin, version})。命令内 require(alias).start() 得到跟这次 Call 的 ToolHandle,handle.invoke/interact 由内核路由到目标 runtime 通道。
3. 探针参考实现
check/ 目录有 Go / .NET / Python 的最小探针(spawn → 就绪行 → /calls → /reset),可直接照抄到各独立仓库。
Node 用法
import { spawnTestHost } from '@syllm/brickly-test-host'
const host = await spawnTestHost() // 拉起子进程
const client = new HostBrickStorageClient(host.dataEndpoint, host.runtimeToHostToken)
await host.setFault({ path: '/brickly.runtime.v1.BrickStorageService/KvSet', brickCode: 'LIMIT_EXCEEDED' })
const calls = await host.recordedCalls()
await host.reset()
await host.close() // 关 stdin,等退出(超时自动 SIGKILL)进程内装配(不需要子进程隔离时):startTestHost() 返回 { host, control, endpoint, spawnCredentials, close }。
本地验证
npm run check --workspace @syllm/brickly-test-host
# 或仓库根:npm run test-host:check执行:构建 bundle → Node 库自检 → Node spawn+gRPC 全链路 → Go/.NET/Python spawn 探针(工具链缺失则跳过该项)。
