rtk CLI 过滤器的测试体系:从单元测试到 Token 精度验证的完整策略
rtk(Rust Token Killer)是一个通过过滤、分组、截断和去重来压缩命令输出的 CLI 代理,其核心价值主张——"在常见开发命令上减少 60–90% 的输出"——必须靠一套严密的测试体系来兑现。本文基于仓库内的测试策略规则 cli-testing.md 展开,完整讲解 RTK 的五层测试结构:单元测试、Token 精度测试、跨平台测试、集成测试与性能测试,并给出可直接复制的测试模板、夹具(fixture)捕获工作流与合并前检查清单,帮助你在为 RTK 新增或修改过滤器时,做到"既有可复现的实操步骤,又有源码级依据"。
一、测试金字塔总览:五层测试各司其职
RTK 的测试策略按优先级和触发时机分为五层,每一层对应不同的变更风险:
| 测试层 | 优先级 | 触发时机 | 测试位置 |
|---|---|---|---|
| 单元测试(Unit Testing) | 关键 | 所有过滤器变更、输出格式修改 | 与被测文件同目录的 #[cfg(test)] mod tests |
| Token 精度测试(Token Accuracy) | 关键 | 所有过滤器实现、token 节省声明 | 同单元测试模块 |
| 跨平台测试(Cross-Platform) | 关键 | Shell 转义变更、命令执行逻辑 | #[cfg(target_os)] 条件编译 |
| 集成测试(Integration) | 重要 | 新过滤器、命令路由变更、发布准备 | tests/ 顶层文件 |
| 性能测试(Performance) | 重要 | 性能相关变更、发布准备 | hyperfine / /usr/bin/time 外部基准 |
这个分层的背后是 RTK 的命令代理架构:main.rs 通过 Clap 的 Commands 枚举把 CLI 命令路由到 src/cmds/*/ 下的专用过滤模块,每个模块执行底层命令并压缩其输出(见 CLAUDE.md 的架构说明)。正因为输出正确性完全依赖各过滤器,"每个过滤器都要被单独验证"才成为测试体系的第一原则。
二、单元测试:与过滤器共置的测试块
基本写法
单元测试采用最朴素的形式:在与过滤器同一文件内放置 #[cfg(test)] mod tests 块,用 assert_eq!/assert! 直接对期望输出做断言:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_log_output() {
let input = "abc1234 fix: handle empty commit\ndef5678 feat: add filter\n";
let output = filter_git_log(input);
assert_eq!(output, "abc1234 fix: handle empty commit\ndef5678 feat: add filter");
}
}
这一约定在仓库中得到了全面践行:从源码结构看,src/cmds/ 下几乎每个过滤器文件都带有测试模块,例如 git.rs 中就有 3 处 #[cfg(test)] 块,而 cargo、php、js、system 等子目录下的 50 余个命令文件基本都有 1–2 处,测试与实现"同文件共置"是整个代码库的统一风格。
两种夹具策略并存
RTK 的单元测试存在两种夹具(fixture)模式,根据过滤器的实际需求选择:
- 内联字面量字符串(
src/cmds/**单元测试中最常见)——在测试体内直接构造一小段有代表性的字符串,适合快速覆盖某种特定格式或边界情况。src/cmds/git/git.rs、src/cmds/git/gh_cmd.rs等处广泛使用此模式。 - 真实捕获夹具 +
include_str!——当原始输出体积大、或格式敏感到内联字符串已经不可读、容易与现实漂移时使用。参考范本是 mvn_cmd.rs:该文件是include_str!夹具模式的"标准答案",当前实际包含 40 余处include_str!调用(文档写作时记载为 23+,此后仍在增长),覆盖mvn test通过/失败/多失败/编译错误、mvnd(Maven Daemon)reactor 通过/失败、expected快照对比等多种场景。这些夹具位于tests/fixtures/,例如:- mvn_test_pass_slice_raw.txt
- mvn_test_multifail_slice_raw.txt
- mvnd_reactor_pass_raw.txt(另有配套
mvnd_reactor_pass_expected.txt期望输出) - gradlew_build_raw.txt
- glab_mr_list_raw.json
何时使用
- 每个新过滤器:覆盖常见用例,外加至少一个边界用例(空输入、错误输出);
- 输出格式变更:过滤器逻辑变化时,同步更新相关的
assert_eq!期望值; - 回归检测:一旦输出体积或格式让手写字符串变得脆弱或失真,就优先切换到真实夹具(
include_str!),而不是继续加长内联字符串。
夹具驱动的完整工作流
以新增一个 include_str! 模式的 mvn 测试为例,完整流程是"捕获 → 写测试 → 运行"三步:
# 1. 捕获真实输出作为夹具(仅 include_str! 模式需要)
mvn test > tests/fixtures/mvn_test_example_raw.txt
# 2. 在被测模块内追加测试
cat >> src/cmds/jvm/mvn_cmd.rs <<'EOF'
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mvn_test_example() {
let input = include_str!("../../../tests/fixtures/mvn_test_example_raw.txt");
let output = filter_mvn_test(input);
assert!(output.contains("FAILED") || output.contains("PASSED"));
}
}
EOF
# 3. 运行该测试
cargo test test_mvn_test_example
注意夹具路径是相对源文件的 ../../../tests/fixtures/(src/cmds/jvm/ 上跳三级到仓库根),include_str! 会在编译期把夹具内容嵌入二进制,因此新增夹具文件后无需任何运行时配置。
三、Token 精度测试:60% 是发布底线
这是 RTK 测试体系中最具项目特色的一层:所有过滤器都必须用真实夹具验证其 token 节省声明。RTK 的全部产品价值就是压缩率,如果测试只验证"不 panic"而不验证"省了多少",核心价值主张就成了无法证伪的口号。
Token 计数测试模板
#[cfg(test)]
mod tests {
fn count_tokens(text: &str) -> usize {
text.split_whitespace().count()
}
#[test]
fn test_git_log_savings() {
let input = "..."; // 内联字符串或 include_str! 夹具
let output = filter_git_log(input);
let input_tokens = count_tokens(input);
let output_tokens = count_tokens(&output);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"Git log filter: expected ≥60% savings, got {:.1}%",
savings
);
}
}
关于 count_tokens 有两个值得注意的实现事实:
- 该辅助函数在每个测试模块内各自重复定义,而不是放在共享的
tests/common/mod.rs中——从源码结构看,fn count_tokens分散在 mvn_cmd.rs、gradlew_cmd.rs、gh_cmd.rs、glab_cmd.rs、git.rs、sbt_cmd.rs 等多个文件中。编写新测试时不要假设存在共享 helper,直接本地定义即可。 - 测试侧的
split_whitespace()词数估算与产品侧的计量口径并不相同:RTK 本身不捆绑分词器,tracking.rs 用bytes / 4来估算 token(见 CLAUDE.md)。也就是说测试衡量的是压缩比(词数比),而运行时统计的是字节比,两者方向一致但口径独立。
夹具制作:用真实命令输出,拒绝合成数据
# 捕获真实输出
git log -20 > tests/fixtures/git_log_raw.txt
cargo test 2>&1 > tests/fixtures/cargo_test_raw.txt
gh pr view 123 > tests/fixtures/gh_pr_view_raw.txt
pnpm list > tests/fixtures/pnpm_list_raw.txt
# 然后在测试中使用:
# let input = include_str!("../tests/fixtures/git_log_raw.txt");
tests/fixtures/ 目录现有约 70 个夹具文件,覆盖 mvn/mvnd、gradlew、ctest、glab、phpstan、dotnet、sbt、aws 等生态的真实输出(含 .gz 压缩的大文件和 expected 期望输出对),是这一原则的集中体现。
节省率目标:单一底线,而非逐命令表格
文档对节省率目标做了明确的纪律约束:
- 不存在逐过滤器的百分比表格,只有一条强制底线:≥60% 节省是发布阻断项(release blocker),与 CLAUDE.md 中"Pre-commit Gate / 性能目标"的约定一致(该项目对 bash 输出声明 60–90% 的压缩区间);
- 各过滤器通常会大幅超过这条底线,但不要在测试中断言具体的逐命令百分比(例如"gh pr view 达 87%"),除非你已用该过滤器自己的夹具实测过真实数字——断言阈值因过滤器而异,而文档里罗列杜撰数字的表格会立刻腐烂(rot);
- 发布阻断:任何过滤器的节省率跌破 60%,必须在合并前查明原因并修复。
四、跨平台测试:Shell 转义的三分支断言
RTK 需要同时工作于 macOS(zsh)、Linux(bash)、Windows(PowerShell),三者的 Shell 转义规则不同,因此涉及 Shell 转义与命令执行逻辑的变更必须触发跨平台测试。
用 cfg 按平台条件编译期望值
测试策略文档给出的模式是:为每个平台定义一份期望 Shell 常量,并在同一测试内按目标 OS 分支断言:
#[cfg(target_os = "windows")]
const EXPECTED_SHELL: &str = "cmd.exe";
#[cfg(target_os = "macos")]
const EXPECTED_SHELL: &str = "zsh";
#[cfg(target_os = "linux")]
const EXPECTED_SHELL: &str = "bash";
#[test]
fn test_shell_escaping() {
let cmd = r#"git log --format="%H %s""#;
let escaped = escape_for_shell(cmd);
#[cfg(target_os = "windows")]
assert_eq!(escaped, r#"git log --format=\"%H %s\""#);
#[cfg(not(target_os = "windows")]
assert_eq!(escaped, r#"git log --format="%H %s""#);
}
这里 escape_for_shell 是规则文档为转义类函数定义的测试模式示例名,用于演示"一个测试函数 + 多个 #[cfg] 断言分支"的写法;从源码结构看,仓库中并没有名为 escape_for_shell 的公开函数(路径转义相关的实现散落在如 tee.rs 的模块内部),落地时请替换为你实际被测的转义函数。
各平台 Shell 差异速查
| 平台 | Shell | 引号转义 | 路径分隔符 |
|---|---|---|---|
| macOS | zsh | '单引号' 或 "双引号" |
/ |
| Linux | bash | '单引号' 或 "双引号" |
/ |
| Windows | PowerShell | `反引号 或 "双引号" |
\ |
测试执行的平台分工
# Linux/macOS(主力平台):本地直接跑
cargo test
Windows 通过 CI 覆盖:信任 CI/CD 流水线的 Windows 作业,或在手边有 Windows 机器时手动验证。CLAUDE.md 也把"过度测试跨平台行为"列为要避开的兔子洞——本地覆盖 macOS + Linux,Windows 交给 CI。
五、集成测试:tests/ 顶层文件与 #[ignore] 真进程测试
顶层集成测试的分工
集成测试以顶层文件形式放在 tests/ 目录(不随 src/ 共置),当前仓库包含:
- grep_context_test.rs、grep_faithful_format_test.rs、search_compress_test.rs、search_error_test.rs、search_faithful_test.rs —— 覆盖搜索/grep 压缩、忠实格式化等横切行为;
- guard_integration_test.rs —— 守护(guard)行为,例如"当过滤反而会让极小输入膨胀时输出原始内容"以及"不阻塞真实压缩"等场景,会借助
tempfile初始化临时 git 仓库并调用rtk二进制; - copilot_selfheal_test.rs、pipeline_stdin_test.rs —— 自愈与管道 stdin 行为。
这些测试验证的是横切行为(search/grep 压缩、guard rails、忠实格式化)而非单个过滤器模块,其中多个测试同时使用 tests/fixtures/ 中真实捕获的 aws/glab/gradlew/mvn/phpstan/dotnet 输出与各自的内联用例。
#[ignore] 真进程测试
真正拉起 RTK 二进制、执行真实命令的测试标记为 #[ignore],避免默认测试跑(它依赖已安装的 rtk 二进制和 git 仓库):
#[test]
#[ignore] // 运行方式:cargo test --ignored
fn test_real_git_log() {
// 依赖:
// 1. RTK 二进制已安装(cargo install --path .)
// 2. 可用的 Git 仓库
let output = std::process::Command::new("rtk")
.args(&["git", "log", "-10"])
.output()
.expect("Failed to run rtk");
assert!(output.status.success());
assert!(!output.stdout.is_empty());
// 验证输出是压缩后的(而非原始 git 输出)
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.len() < 5000, "Output too large, filter not working");
}
这类断言的精髓在于反向验证压缩生效:不是检查输出等于某段字符串,而是检查"输出必须小于某个体积上界"——一旦过滤器失效、原始输出直通,测试立即失败。
运行顺序
# 1. 本地安装 RTK
cargo install --path .
# 2. 运行全部测试,包含 tests/*.rs 顶层集成测试
cargo test --all
# 3. 运行被忽略的(真进程)集成测试
cargo test --ignored
# 4. 运行指定测试
cargo test --ignored test_real_git_log
此外仓库还提供了 scripts/test-all.sh 作为冒烟脚本(需要已安装的 RTK 二进制),与 CLAUDE.md 中记录的 bash scripts/test-all.sh 用法一致。
何时运行
- 发布前:始终运行集成测试;
- 过滤器变更之后:确认真实命令输出下过滤器仍然有效;
- Hook 变更之后:确认 Claude Code 集成仍正常工作(仓库
hooks/目录维护了 claude、copilot、cursor 等各 Agent 的重写钩子,src/hooks/则是对应的 Rust 侧实现)。
六、性能测试:<10ms 启动、<5MB 内存
RTK 的性能目标是启动时间 <10ms、内存占用 <5MB、二进制体积 <5MB。这一约束直接影响了架构决策(CLAUDE.md 明确"无 async,单线程设计,启动 <10ms"),因此性能测试不是可选项。
用 hyperfine 基准测试启动时间
# 安装 hyperfine
brew install hyperfine # macOS
cargo install hyperfine # 或经 cargo 安装
# 基准对比:RTK vs 原始命令
hyperfine 'rtk git status' 'git status' --warmup 3
# 应显示 RTK 启动 <10ms
# 示例输出:
# rtk git status 6.2 ms ± 0.3 ms
# git status 8.1 ms ± 0.4 ms
内存占用测量
# macOS
/usr/bin/time -l rtk git status
# 查看 "maximum resident set size",应 <5MB
# Linux
/usr/bin/time -v rtk git status
# 查看 "Maximum resident set size",应 <5000 kbytes
性能回归检测:before/after 对比法
# 变更前
hyperfine 'rtk git log -10' --warmup 3 > /tmp/before.txt
# 变更后
cargo build --release
hyperfine 'target/release/rtk git log -10' --warmup 3 > /tmp/after.txt
# 对比
diff /tmp/before.txt /tmp/after.txt
# 若启动时间增加 >2ms,需要调查原因
性能目标与验证手段
| 指标 | 目标 | 验证方式 |
|---|---|---|
| 启动时间 | <10ms | hyperfine 'rtk <cmd>' |
| 内存占用 | <5MB | time -l rtk <cmd> |
| 二进制体积 | <5MB | ls -lh target/release/rtk |
七、测试组织:目录结构与最佳实践
完整的测试目录结构如下:
rtk/
├── src/
│ ├── cmds/
│ │ ├── git/
│ │ │ ├── git.rs # 过滤器实现
│ │ │ │ └── #[cfg(test)] mod tests { ... }
│ │ ├── jvm/ # gradlew、mvn — include_str! 夹具的参考范本
│ │ ├── php/ # php, artisan, phpunit, phpstan, pest, paratest, ecs, pint
│ │ └── ...
│ ├── core/ # 共享基础设施
│ ├── hooks/ # Hook 系统
│ └── analytics/ # Token 节省分析
├── tests/
│ ├── fixtures/ # 真实捕获的命令输出
│ │ ├── mvn_test_pass_slice_raw.txt
│ │ ├── gradlew_build_raw.txt
│ │ ├── glab_mr_list_raw.json
│ │ └── ...
│ ├── grep_context_test.rs # 顶层集成测试
│ ├── grep_faithful_format_test.rs
│ ├── guard_integration_test.rs
│ ├── search_compress_test.rs
│ ├── search_error_test.rs
│ └── search_faithful_test.rs
最佳实践总结:
- 单元测试:嵌入模块内(
#[cfg(test)] mod tests),与过滤器共置; - 夹具:小而快的用例优先内联字符串;用例变大或格式敏感后切换到
tests/fixtures/的include_str!真实输出——模式参照 mvn_cmd.rs; count_tokenshelper:目前是各测试模块各自重复定义,不要假设存在共享的tests/common/mod.rs;- 集成测试:顶层
tests/*.rs文件,部分含#[ignore]标记的真进程测试。
新增过滤器时的完整检查清单见 src/cmds/README.md 的"Adding a New Command Filter"章节。
八、测试检查清单:从实现到发布
新增或修改过滤器时,按阶段逐项核对:
实现阶段
- [ ] 在该过滤器自己的
#[cfg(test)] mod tests块中编写单元测试(内联字符串,或较大/真实输出用include_str!夹具) - [ ] 添加 token 精度测试(用本地定义的
count_tokens验证 ≥60% 节省) - [ ] 测试跨平台 Shell 转义(如适用)
质量检查
- [ ]
cargo test --all全部通过 - [ ]
cargo test --ignored集成测试通过 - [ ] 用
hyperfine基准测试启动时间(<10ms)
合并前
- [ ] 所有测试通过(
cargo test --all) - [ ] token 节省 ≥60% 已验证
- [ ] 跨平台测试通过(Linux + macOS)
- [ ] 性能基准通过(启动 <10ms)
发布前
- [ ] 集成测试通过(
cargo test --ignored) - [ ] 性能回归检查(hyperfine before/after 对比)
- [ ] 内存占用验证(
time -l下 <5MB) - [ ] 跨平台 CI 通过(Linux + macOS + Windows)
这些检查项最终汇入 CLAUDE.md 定义的"Pre-commit Gate"——每次 Rust 文件编辑后必须通过 cargo fmt --all && cargo clippy --all-targets && cargo test --all 三道闸口,clippy 警告零容忍。
九、常见测试模式与反模式
模式一:内联夹具 + Token 精度(小而快的合成用例)
#[cfg(test)]
mod tests {
use super::*;
fn count_tokens(text: &str) -> usize {
text.split_whitespace().count()
}
#[test]
fn test_output_format() {
let input = "raw command output here";
let output = filter_cmd(input);
assert_eq!(output, "expected filtered output");
}
#[test]
fn test_token_savings() {
let input = "raw command output here";
let output = filter_cmd(input);
let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0);
assert!(savings >= 60.0, "Expected >=60% savings, got {:.1}%", savings);
}
}
模式二:include_str! 夹具(真实捕获输出)
适用于输出足够大或格式敏感、手写字符串会与现实漂移的过滤器(见 mvn_cmd.rs):
#[test]
fn test_mvn_test_pass() {
let input = include_str!("../../../tests/fixtures/mvn_test_pass_slice_raw.txt");
let output = filter_mvn_test(input);
assert!(output.contains("BUILD SUCCESS"));
}
模式三:边界用例(过滤器健壮性)
#[test]
fn test_empty_input() {
let output = filter_cmd("");
assert_eq!(output, "");
}
#[test]
fn test_malformed_input() {
let malformed = "not valid command output";
let output = filter_cmd(malformed);
// 二选一:
// 1. 返回尽力而为的过滤输出,或
// 2. 原样返回输入(fallback)
// 两者都可接受——关键是绝不能 panic!
assert!(!output.is_empty());
}
#[test]
fn test_unicode_input() {
let unicode = "commit 日本語メッセージ";
let output = filter_cmd(unicode);
assert!(output.contains("commit"));
}
#[test]
fn test_ansi_codes() {
let ansi = "\x1b[32mSuccess\x1b[0m";
let output = filter_cmd(ansi);
// 应去除或保留 ANSI 码,但不能弄坏输出
assert!(output.contains("Success") || output.contains("\x1b[32m"));
}
"失败时回退、绝不 panic"与 CLAUDE.md 编码规则中的 fallback 模式(过滤器失败时原样执行原始命令)一脉相承。
模式四:集成测试(端到端行为)
#[test]
#[ignore]
fn test_real_command_execution() {
let output = std::process::Command::new("rtk")
.args(&["cmd", "args"])
.output()
.expect("Failed to run rtk");
assert!(output.status.success());
assert!(!output.stdout.is_empty());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.len() < 5000, "Output too large");
}
反模式清单
❌ 用硬编码合成数据测试(合成数据不能反映真实命令输出)→ ✅ 直接断言期望输出:
// ❌ 错误:let input = "commit abc123\nAuthor: John"; 然后对合成输入下结论
// ✅ 正确:
let output = filter_git_log(input);
assert_eq!(output, "expected output");
❌ 跳过跨平台测试(只测当前平台)→ ✅ 用 cfg 覆盖所有平台:
// ✅ 正确——同一测试内按平台分支断言
#[test]
fn test_shell_escaping() {
let escaped = escape("test");
#[cfg(target_os = "windows")]
assert_eq!(escaped, "\"test\"");
#[cfg(not(target_os = "windows"))]
assert_eq!(escaped, "test");
}
❌ 忽视性能回归(只测功能、不做性能跟踪)→ ✅ 基准并跟踪性能:
# ✅ 正确——变更前/后基准对比
hyperfine 'rtk cmd' --warmup 3 > /tmp/before.txt
# 做修改
cargo build --release
hyperfine 'target/release/rtk cmd' --warmup 3 > /tmp/after.txt
diff /tmp/before.txt /tmp/after.txt
❌ 接受 <60% 的 token 节省(无节省率验证的测试毫无意义)→ ✅ 验证节省声明:
// ✅ 正确——验证 ≥60% 节省
#[test]
fn test_token_savings() {
let savings = calculate_savings(input, output);
assert!(savings >= 60.0, "Expected ≥60%, got {:.1}%", savings);
}
十、小结
RTK 的测试策略可以浓缩为一句话:每个过滤器都要被"格式正确、压缩达标、平台无偏、性能不回归"四个维度独立验证。内联字符串与 include_str! 真实夹具两种策略并存、count_tokens 本地重复定义、集成测试用 #[ignore] 隔离真进程依赖——这些看似"不优雅"的约定,实际上都是为了降低新增过滤器的认知成本:任何贡献者只需要打开 mvn_cmd.rs 这一个参考文件,就能照抄出一套完整的、满足发布标准的测试。对于正在为代理型 CLI 工具设计测试体系的开发者,这套"以核心指标(压缩率)为发布阻断项、以真实夹具为回归基准"的做法尤其值得借鉴。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00