# 测试手册 catbus 的测试分三层: | 层 | 命令 | 联网 | 在哪里跑 | 目的 | |---|---|---|---|---| | 离线:单元与对拍 | `npm test` | 禁止 | 本机、CI | 逻辑正确;请求构造和签名与上游逐字节一致 | | 在线 | `npm run test:e2e` | 是,用本机登录态 | 只在开发者本机 | 真实平台上只读命令能跑通,归一化字段结构正确 | | 冒烟 | `node scripts/smoke.mjs` | 否 | CI 的全部目标系统 | 打好的包在每个系统上都能安装、加载原生依赖、运行 | 规范依据是 AGENTS 7.5「移植与对拍测试」。怎么新增命令、怎么生成对拍数据,见 [CONTRIBUTING.md](CONTRIBUTING.md)。 ## 1. 准备 ```bash npm ci npm run assets:jd # 京东验证码模型:jd.test.ts 的 JCAP 求解器要用 npm run assets:ocr # ddddocr 模型:bilibili-geetest.test.ts 的极验点选识别要用 ``` 模型文件不进 git,取回一次之后就一直在 `packages/assets-*/models/` 里。 ## 2. 离线测试:单元与对拍 ### 2.1 运行 ```bash npm test # 全部离线测试 npm run typecheck # 类型检查:vitest 不做类型检查,要单独跑 npx vitest run tests/bilibili.test.ts # 只跑一个文件 npx vitest run tests/xhs.test.ts -t 对拍 # 只跑名字匹配的用例 npx vitest # watch 模式 ``` 测试直接跑 `src/` 下的 TypeScript,不需要先 `npm run build`。 配置在 [vitest.config.ts](../vitest.config.ts): - 只收 `tests/**/*.test.ts`; - 预加载 [tests/setup.ts](../tests/setup.ts); - 单个用例超时 60 秒:对拍用例要在 vm 里跑上游的签名 JS,慢的 CI 机器上会超过默认的 5 秒; - 时区固定为 `Asia/Shanghai`:对拍数据在北京时间下生成,签名里常有按本地时间格式化的时间戳(例如京东 h5st)。 ### 2.2 禁止联网 [tests/setup.ts](../tests/setup.ts) 把 `core/http` 的发送器换成一个直接抛错的函数。离线测试里只要有一条请求漏到真实网络,就会报: ``` 测试里不允许联网:GET https://... ``` 需要请求的测试有两种办法:用 `replay()` 按对拍数据回放(2.4),或者用 `mockSender()` 临时换一个按 URL 返回假响应的发送器(2.6)。 ### 2.3 目录与文件 | 路径 | 内容 | |---|---| | `tests/cli.test.ts` | 全局命令、四层帮助、命令判定(NOT_IMPLEMENTED / UNSUPPORTED / USAGE)、core 实现的 auth 命令 | | `tests/core.test.ts` | errors、日志打码、选项、hints、doctor、原子写入、auth store、config、http、vm、登录轮询、cookie 罐、`jsonLoads` | | `tests/registry.test.ts` | 注册表约束、handler 名与命令对应、`docs/capabilities.md` 是否最新、`package.json` 是否精确钉住模型包版本 | | `tests/runtime.test.ts` | 身份与游客态、危险操作确认、分页、长连接、输出格式、发布选项 | | `tests/golden-core.test.ts` | core 的对拍:Python 编码函数、curl_cffi 的 URL 拼接、确定性随机数与时钟 | | `tests/
.test.ts` | 每个平台的对拍测试与命令测试 | | `tests/bilibili-geetest.test.ts` | B 站极验:w 参数、验证链路、点选识别(ddddocr)、人工兜底页面 | | `tests/golden/
/*.json` | 对拍数据,由 `scripts/golden/
/gen.py` 生成,不手改 | | `tests/golden.ts` | 对拍工具:`loadCase`、`replay`、`expectRequests`、`makeCtx` | | `tests/helpers.ts` | `useTempHome()`:每个测试一个临时 `CATBUS_HOME`;`cli(...argv)`:在进程内跑一次 catbus,捕获 stdout / stderr 并解析信封 | 各平台的对拍数据文件数(`ls tests/golden/
| wc -l`,统计于 2026-09-29): | 平台 | 对拍数据 | 生成脚本 | |---|---:|---| | core | 3 | `scripts/golden/core/gen.py` | | xhs | 56 | `scripts/golden/xhs/gen.py` | | douyin | 82 | `scripts/golden/douyin/gen.py`(末尾再执行 `gen_more.py`、`gen_gap.py`) | | tiktok | 100 | `scripts/golden/tiktok/gen.py` | | bilibili | 85 | `scripts/golden/bilibili/gen.py` | | kuaishou | 79 | `scripts/golden/kuaishou/gen.py` | | weibo | 19 | `scripts/golden/weibo/gen.py` | | xianyu | 20 | `scripts/golden/xianyu/gen.py` | | taobao | 12 | `scripts/golden/taobao/gen.py` | | jd | 46 | `scripts/golden/jd/gen.py` | | x | 60 | `scripts/golden/x/gen.py` | | **合计** | **562** | | 大多数文件是一条请求序列;也有一些记录的是纯计算的中间结果(例如验证码图像处理、指纹块),由对应的测试逐项比较。 ### 2.4 对拍测试怎么工作 一份对拍数据(`tests/golden/
/ /gen.py` 里加用例:`case('<名字>', <调用上游的函数>, **input)`,需要的话在 `respond(req)` 里按 URL 给出假响应。
2. 运行 `.golden/ /bin/python scripts/golden/ /gen.py`(Windows 为 `.golden\ \Scripts\python`),生成 `tests/golden/ /<名字>.json`。
3. 在 `tests/ .test.ts` 的用例表里加上同名的 TS 调用。多数平台有一张 `CASES` 表(用例名 → TS 侧的等价调用,值的具体形状各平台略有不同,照同文件里已有的写),循环里对每个用例做 `replay` + `expectRequests`。
4. `npx vitest run tests/ .test.ts`。
5. 用 `git diff tests/golden/ ` 确认只有预期的用例变了。
### 2.6 命令测试(非对拍)
上游没有对应方法、catbus 自己补的逻辑(参数解析、归一化、分页、错误映射、上游没有的请求),用命令测试覆盖:`mockSender` 按 URL 回假响应,`deterministic()` 固定时钟,直接调用 handler。例子见 `tests/bilibili.test.ts` 的 `runCommand`:
```ts
const restoreRand = deterministic({ now: NOW })
const restore = mockSender((p) => fakeResponse(route(p.url, p.body), { headers: [['content-type', 'application/json']], url: p.url }))
try {
const result = await commands.itemRelated(ctx)
// 断言 result 与发出的请求
} finally {
restore()
restoreRand()
}
```
测 CLI 层的行为(退出码、信封、帮助、确认)用 `tests/helpers.ts` 的 `useTempHome()` 和 `cli()`:
```ts
useTempHome()
it('planned 端报 NOT_IMPLEMENTED', async () => {
const r = await cli('xianyu', 'item', 'get', 'x', '-e', 'app')
expect(r.code).toBe(4)
expect(r.env.error.code).toBe('NOT_IMPLEMENTED')
})
```
### 2.7 常见失败
| 现象 | 原因与处理 |
|---|---|
| `测试里不允许联网:<方法> 」) |
| 前面的命令没拿到需要的对象 | 跳过(「前面的命令没有拿到 <参数名>」) |
| `AUTH_REQUIRED` | 跳过:主站已经登录,这是子站点(`--scope`)没登录 |
| `RISK_CONTROL` | 跳过:风控取决于账号和近期的请求,不是代码问题 |
| `NOT_IMPLEMENTED` | 跳过:默认参数落在了注册表 note 写明「规划中」的取值上 |
| `UNSUPPORTED`,且命令有 note | 跳过:参数不在 note 写明的支持范围内 |
| 其他错误 | 失败 |
| 成功 | 校验:`ok`、`platform`、`account`;`data` 符合 AGENTS 6.2 的结构([tests/e2e/shapes.ts](../tests/e2e/shapes.ts),zod strict 对象,多一个字段、少一个字段都算错);分页命令的 `page` 带 `has_more`,其他命令的 `page` 为 null;`item search`、`user search`、`feed list`、`keyword hot` / `suggest`、`live list` / `search` 不能返回空列表(TikTok 的 `live list` 是「关注的人在播」,允许为空) |
### 3.3 报告
每个平台跑完写一份 `.e2e/ .json`(不进版本控制)。每条命令一项:
| 字段 | 内容 |
|---|---|
| `command`、`argv` | 命令与实际用的参数 |
| `code`、`error` | 退出码与错误 |
| `count` | 列表的条数 |
| `empty_fields` | 在**所有**元素里都是 null 或 `[]` 的字段,用来发现归一化漏取的字段 |
| `page` | 分页信息 |
| `data` | 返回的数据(列表只留前 3 条) |
`empty_fields` 里出现的字段,先对照 [各平台说明](../docs/platforms/README.md) 的「已知限制」:上游接口本来就不返回的字段属于已知情况,其余的多半是 `normalize.ts` 取错了键。
报告里是真实账号返回的数据(昵称、ID、私信会话等),**不要提交,也不要整份贴到 issue 里**。
### 3.4 加用例
在线测试没有手写的用例表:新实现的只读命令会自动加入。只有下面几种情况要改 `tests/e2e/`:
- 新的只读 action(例如扩展命令 `item subtitles` 的 `subtitles`):加进 `live.e2e.ts` 的 `READ_ACTIONS`;
- 新的位置参数名:在 `argValue()` 里说明从池子里取什么;
- 新的输出类型:在 `shapes.ts` 的 `SHAPES` 里加校验器(否则报「没有 <类型> 的校验器」);想让它的结果给后面的命令用,在 `harvest()` 里收进池子;
- 某条命令在真实平台上不该为空,或者允许为空:改 `NON_EMPTY` / `MAY_BE_EMPTY`。
### 3.5 注意与常见失败
- 在线测试会用你的账号发大量请求,可能触发风控。真机验证期间京东的测试账号被封过,建议用专门的测试账号,不要频繁连续跑。
- 某个平台的全部命令都跳过:先确认 `catbus auth status` 返回 `logged_in: true`。
- 结构校验失败:错误信息里是 zod 的前 5 条 issue,常见的是时间不是带时区的 ISO 8601、计数是字符串、多了平台私有字段。对照 `.e2e/ .json` 里的 `data` 修 `normalize.ts`。
- 「真实平台上不该是空列表」:先手动跑一次同样的命令,看是搜索词不合适、账号本身没有数据,还是接口换了字段。
- 各平台目前的已知问题见 [各平台说明](../docs/platforms/README.md) 的「已知限制」。
## 4. CI 与冒烟测试
### 4.1 CI 做了什么
[.github/workflows/ci.yml](workflows/ci.yml) 在推送到 `master`、提交 PR 时触发;推送 `v*` tag 时,[release.yml](workflows/release.yml) 也会先调用它跑完整 CI,再发布 CI 里打好、测过的同一份 tarball,并从 CHANGELOG 生成 GitHub Release(发版步骤见 [AGENTS.md 7.2](../AGENTS.md#72-打包与分发))。
| job | 环境 | 步骤 |
|---|---|---|
| `test` | ubuntu / windows / macos × Node 22 / 24 / 26 | `npm ci` → `assets:jd` → `assets:ocr`(模型按 key 缓存)→ `typecheck` → `npm test` |
| `pack` | ubuntu,Node 24 | `npm pack` 打出 `catbus-cli`、`@cv-cat/catbus-assets-jd`、`@cv-cat/catbus-assets-ocr` 三个 tarball(prepack 会先构建、先取回并校验模型),连同 `scripts/smoke.mjs` 上传为 artifact |
| `smoke` | 8 个目标系统 × Node 22 / 24 | 全局安装三个 tarball,运行 `node scripts/smoke.mjs` |
CI 只跑离线测试,不跑在线测试。
`smoke` 覆盖 AGENTS 7.2 的全部目标系统:
| target | runner |
|---|---|
| win32-x64 | `windows-latest` |
| win32-arm64 | `windows-11-arm` |
| darwin-arm64 | `macos-latest` |
| darwin-x64 | `macos-15-intel` |
| linux-x64-gnu | `ubuntu-latest` |
| linux-arm64-gnu | `ubuntu-24.04-arm` |
| linux-x64-musl | `ubuntu-latest` 上的 `node:<版本>-alpine` 容器 |
| linux-arm64-musl | `ubuntu-24.04-arm` 上的 `node:<版本>-alpine` 容器 |
Alpine 容器里跑不了 JS action(arm64 尤其如此),所以 musl 两项直接用 `docker run`。
### 4.2 冒烟测试查什么
[scripts/smoke.mjs](../scripts/smoke.mjs) 对**已经全局安装**的 `catbus` 运行下面几项,数据目录用一个临时的 `CATBUS_HOME`:
| 检查项 | 通过条件 |
|---|---|
| `version` | 退出码 0,报告的 Node 版本就是当前的 Node |
| `doctor` | 全部检查项通过:Node 版本、数据目录可写且权限正确、签名 vm 能执行、wreq-js / @napi-rs/canvas / onnxruntime-web 能加载、两个模型包就位 |
| 模型包 | `doctor` 的 `assets-jd`、`assets-ocr` 两项通过 |
| `platforms` | 返回 10 个平台 |
| `--help` | 输出里有 `catbus