Repository navigation
1822 项断言全绿:我们怎么测试一个 Agent 管控平台 #30
01men
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
1822 项断言全绿:我们怎么测试一个 Agent 管控平台
先给三个数字,都来自 CI 的同一份日志:
scripts/selftest.mjs:10071 行,107 个测试分节这不是炫技。管控平台的测试难点跟业务系统完全不是一回事——业务系统测的是"功能对不对",管控平台测的是"拦住的时候真的拦住了,放行的时候真的没顺手多放"。前者漏测是功能缺失,后者漏测是安全事故。
下面是我们踩出来的做法,包括我们测砸过的部分。
一、起一个真实例,别 mock 自己的边界
管控平台最典型的错误测法是:把鉴权中间件 mock 掉,然后测业务逻辑。
这条路一开始就走歪了。因为这类平台的绝大多数价值就发生在中间件里——令牌受众校验、主体匹配、权限点判定、租户隔离。把这些 mock 掉,你测的是去掉安防的银行大堂。
所以
selftest.mjs的第一件事是 spawn 一个隔离实例:然后所有断言走真实 HTTP:
fetch打真实端点,带真实 Bearer 头,读真实 JSON 响应。代价很明显:慢。一次完整跑要几十秒,出问题时的报错是一坨 JSON 而不是干净的行号。收益也很明显:断言里写的
403就是线上会拿到的403。有一条纪律我们写进了脚本注释:断言的对象必须是外部可观测的行为,不是内部函数返回值。 内部函数变了但 HTTP 契约没变,测试不该红;HTTP 契约变了,测试必须红。
例外只有一个:纯函数引擎。NAS 数据权限的判定引擎我们做了直接单测,因为它是个无状态纯函数,走 HTTP 反而是在测序列化。判断标准是"这层有没有自己的边界",不是"测试好不好写"。
二、107 个分节,按"谁会被打穿"排
1822 条断言不是平铺的清单,是 107 个分节。分节顺序本身有信息量——按攻击面排,不按功能菜单排。
前面几节是身份与权限:
这一段的断言密度最高。原因很直白:身份层错了,后面每一层都是在保护一个已经被攻破的入口。
举个具体的:账号冻结之后,已经发出的 access token 必须在有效期内立刻失效,refresh token 必须同时被拒。这个"联动"很容易做成"下次登录时才检查",看起来能用,实际留了一个 token TTL 长度的窗口。断言写的是冻结后立即打原 token,必须 401。
中段是资产生命周期——MCP 部署与灰度、Skill 扫描与两级审批、Agent 本体生命周期、AI 应用注册、市场验签。
后面是治理的横切面——审计四类日志、告警、成本归因、审批 SLA、数据权限例外与审批闭环。
最后几节是存储与启动。这两块放在最后不是因为不重要,是因为它们最容易被"功能跑通了就行"糊过去。
三、四类不好测的东西
1. 故障注入:不只是测 happy path 的镜像
一个治理平台的错误处理,决定了它是"出事时能说清"还是"出事时全黑"。
我们注入了四类故障,每类都断言失败可见 + 恢复可用:
status=error并计入 error_rate,不静默吞掉;解除后恢复auditPersisted=false(fail-visible 双写)第二类最值得说。审计写失败时,常见做法是重试然后放弃——调用方以为审计记了,其实没有。我们选择反过来:调用继续,但把"审计没落"这件事当场告诉调用方,同时平台侧补记一条。宁可让调用方知道链路有瑕疵,也不给一个"看起来很完整"的假审计。
2. P95 尾延迟闸门:把性能写成断言
性能通常靠人肉压测,靠感觉。我们把它做成了断言:
数字本身不漂亮,但它挡住了一类真实退化:某个改动让登录多了一次串行 IO,功能全对,用户开始抱怨"这系统点一下要等一下"。没有闸门,这种退化会活到线上,然后变成"感觉有点慢"这种无法定位的模糊投诉。
阈值走环境变量(
P95_GATE_LOGIN_MS/P95_GATE_INVOKE_MS),因为 CI 机器的性能和你自己的机器不一样,硬编码会让本地开发天天假红。3. 崩溃恢复:kill -9 之后数据还在吗
存储这块,我们做了 SQLite 后端契约测试。核心断言只有一条,但它比一百条 CRUD 断言有用:
kill -9是故意的,不是SIGTERM。SIGTERM会走优雅关闭,缓存刷盘、连接关闭全都正常执行,测出来的"数据没丢"没有意义——真实事故里进程是被 OOM killer 干掉的。这一节还断言了"JSON 遗留数据回填到 SQLite 后 API 可见",也就是迁移路径本身要在测试里走一遍,不能只测新库。
4. 跨后端契约:同一套断言跑两种存储
业务集合有 JSON 和 SQLite 两种形态。同一批断言在两种后端下都要绿。
这里翻过车:一开始只测 JSON,切到 SQLite 后发现"一人一号"的唯一约束在 JSON 后端靠代码判重、在 SQLite 后端靠 UNIQUE INDEX,两条路径的错误码不一样,前端只认其中一个。断言写细之后才发现。
现在关于唯一约束的断言是四条,覆盖:同 provider 二次绑定被拒、同三方身份跨 corpId 放行(唯一键要含 corpId)、同 tick 连发 6 次重复绑定恰一条成功、绕过服务层直插同键记录被索引拒绝。最后一条是专门写来防止"约束只写在服务层"的。
四、测试自己也会骗人:六个真实教训
这一段是这篇文章里最该被抄走的部分。以下全部是我们自己的历史 bug,脚本注释里都留着日期。
1. 硬编码的版本号,会随着发版双红。
断言里写了
expect version === '1.1.0'。仓库升到 1.2.0 那天,两条断言一起红,看起来像"版本功能坏了",实际是测试自己过期。现在版本号从package.json动态读。2. 固定端口会跟并行实例打架,产生假失败。
测试用过 7313/7316/7318/7319/7322/7399 一组固定端口。有一次两个测试同时跑,端口抢占,出现了 5 条"功能坏了"的假失败——我们花了半天去查一个不存在的 bug。现在子实例用
listen(0)让内核分配端口,取到后立刻释放再绑。想复现历史环境可以SELFTEST_PIN_PORTS=1钉死。3. 测试进程崩掉,会把汇总输出一起吞掉。
收尾阶段的一个晚到的
unhandledRejection(undici keep-alive 连接)能让整个进程挂掉,结果不是"某条断言失败",而是没有任何汇总输出——你只看到 CI 红了,不知道红在哪。现在会捕获、打印、不阻断汇总。4. 用一个数字字面量当"权限点总数",然后发版前才发现对不上。
这类断言的正确写法不是
=== 85,而是"清单里的权限点集合 == 实现里声明的权限点集合"——测的是一致性,不是计数。计数会变,一致性不该变。5. 顺手 mock 掉的对象,会先变成"通过",再变成"没人知道它到底在不在"。
早期有一个 stub,把第三方目录服务的响应写死。后来真实接口改了字段名,stub 没改,测试一路绿——直到线上联调。现在 stub 的字段名从真实文档的示例 JSON 里粘贴,并且单独有一条断言校验 stub 的响应形状。
6. 断言写"应该返回 403",但没写"谁被 403"。
RBAC 那节最后收敛成了矩阵驱动:普通成员直调管理端点,100% 被拒——遍历权限点 × 端点,任何一格放行就是红。这条断言抓到过两次真实的越权。单点断言抓不到,因为漏的那个点你根本没想到去写。
五、我们测不到什么
诚实部分。这 1822 条断言不能证明的事:
多节点。 全部断言跑在单进程单实例上。我们没有做水平扩展,多节点 HA 需要换 Postgres 后端,架构规划在 ADR 里,没有实现。所以"两个节点同时写同一条记录会怎样"这个问题,测试回答不了。
真实上游的长期行为。 模型网关测的是真实转发,但上游是测试 endpoint。上游的限流策略、配额耗尽、模型下线这些行为,测试里是注入的,不是自然发生的。
人的操作。 审批 SLA、二次确认、终审可追溯这些,测的是状态机和 API 契约。真实的"审批人休假两周,单子卡住了怎么办",测不到。
性能的天花板。 P95 闸门是回归防护,不是容量测试。它防退化,不告诉你 500 并发会怎样。
浏览器端到端。 我们有一个基于 jsdom 的页面烟测,但它不是真浏览器。真实的 CSS 布局崩坏、字体加载失败、移动端点不到按钮,这些要靠手工验收。
权限点的语义正确性。 矩阵能证明"该拒的都拒了",不能证明"该给的对的人真的给了"。那取决于权限点划分得对不对,这是设计问题,不是测试问题。
六、如果只抄一条
如果这套东西只能带走一条经验,我会选这个:
把"要拦住什么"写成断言,而不是把"要做出什么"写成断言。
1822 条断言里,一半以上是负数断言——断言某个东西不能发生:没有 token 不能读、被冻结的账号不能续期、审计没落必须透出、目录漂移必须 fail-closed、唯一约束在绕过服务层时依然生效。
正数断言保护的是功能,负数断言保护的是边界。而一个管控平台,本质上就是一堆边界。
项目:github.com/01men/ybkkasi(MIT)· 30 个插件包 / 94 个权限点 / 499 条声明路由 / 98 个 MCP 工具。文中 1822 为 README 与 CI badge 口径,selftest 内建「落地页断言数 = 本次实际断言总数」自校验,本地批次跑得更高时也不让 badge 跟着最大值走,只跟已发布版本。
完整版(带表格与代码块):https://ybkkasi.com/blog/agent-platform-selftest/
仓库:https://github.com/01men/ybkkasi · MIT · 免登演示:https://demo.ybkkasi.com
All reactions