分层测试框架实践:从单元测试到 E2E 的完整链路(Rust/JS/Python CDP)
基于一个 Rust 全栈项目(Server + CLI Client + Tauri Desktop)的真实经验总结。 包含工具链、踩过的坑、给 AI Agent 协作开发的建议。
┌──────────────────────────────────────────────────┐
│ L4: E2E 测试 (55 tests) │
│ Python/CDP → 真实环境 → 全链路验证 │
├──────────────────────────────────────────────────┤
│ L3: Lint (cargo clippy -D warnings) │
├──────────────────────────────────────────────────┤
│ L2: JS 测试 (32 tests, vitest) │
│ 前端逻辑:压缩、编解码、CDP API │
├──────────────────────────────────────────────────┤
│ L1: 单元 + 集成测试 (271 tests, cargo test) │
│ Rust 各 crate:crypto, storage, server, client │
└──────────────────────────────────────────────────┘
| 层级 | 命令 | 何时执行 | 耗时 |
|---|---|---|---|
| L1 | cargo test --workspace |
每次提交前 | ~5s |
| L2 | cd web && npm test |
每次提交前 | ~2s |
| L3 | cargo clippy --workspace -- -D warnings |
每次提交前 | ~3s |
| L4 | E2E 脚本(远程环境) | 部署到测试环境后 | ~5min |
铁律:L1-L3 必须全部通过后才能构建发布。绝不在单元测试挂掉的代码上跑 E2E。
核心工具:
tempfile::TempDir— 自动清理的隔离 SQLite 数据库tokio::test— 异步测试- 子进程集成测试 — 启动完整 server binary,随机端口 + 临时 DB
存储层测试模式:
fn new_test_store() -> (Storage, TempDir) {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("test.db");
let store = Storage::new(&db_path).unwrap();
(store, dir) // dir 在作用域结束时自动清理
}
#[test]
fn test_device_crud() {
let (store, _dir) = new_test_store();
let device = store.create_device("test-device", "token-hash").unwrap();
assert_eq!(device.name, "test-device");
// 建 → 查 → 改 → 删,每步都断言
}Server 集成测试模式:
fn test_server() -> (String, Child, TempDir) {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("test.db");
pre_create_admin(&db_path);
let port = TcpListener::bind("127.0.0.1:0").unwrap()
.local_addr().unwrap().port();
let child = Command::new("cargo")
.args(["run", "-p", "my-server", "--",
"--listen", &format!(":{}", port),
"--test-token", "test123",
"--enable-test-auth"])
.spawn().unwrap();
wait_for_port(port);
(format!("http://127.0.0.1:{}", port), child, dir)
}并发测试模式:
#[tokio::test]
async fn test_concurrent_relay() {
let hub = Arc::new(RelayHub::new());
let mut handles = Vec::new();
for i in 0..20 {
let hub = hub.clone();
handles.push(tokio::spawn(async move {
hub.register_client(format!("client-{}", i)).await;
}));
}
// 超时保护,防止死锁导致测试永远挂起
tokio::time::timeout(Duration::from_secs(5),
futures::future::join_all(handles)
).await.unwrap();
}方法提取测试技术 — 从源码中提取单个方法进行隔离测试:
function extractMethod(source, methodName) {
const regex = new RegExp(`\\b${methodName}\\s*\\(`);
const match = source.match(regex);
// 匹配括号找到参数列表和函数体,返回可独立调用的函数
}
test('showConfirm resolves on respond', async () => {
const app = buildMockApp();
const promise = app.showConfirm('Delete?');
app.__omt_cdp.respond(true);
expect(await promise).toBe(true);
});Fake Timer 测试异步逻辑:
test('waitForModal times out', async () => {
vi.useFakeTimers();
const promise = cdp.waitForModal(5000);
vi.advanceTimersByTime(5001);
await expect(promise).rejects.toThrow('timeout');
vi.useRealTimers();
});架构:
Linux 测试主机
├── Groups A-F: curl/API 测试(直接 SSH 到测试服务器)
├── Groups G/H: CDP Python 脚本(上传到 Windows 运行)
└── Group V: 视觉验证(Xvfb + xfreerdp + Ollama Vision)
Windows 桌面机(Tauri 应用)
├── app.exe + start_cdp.bat(启用 CDP 调试端口)
├── cdp_helper.py + cdp_group_*.py
└── CDP port 9222
测试服务器(Linux)
├── server --test-token xxx --enable-test-auth
├── client daemon(守护进程模式)
└── SQLite 数据库
CDP 测试辅助类(核心):
class CDPSession:
"""通过 Chrome DevTools Protocol 控制 WebView"""
async def js(self, expr, timeout=20):
"""执行 JS 表达式,返回值"""
r = await self.evaluate(expr, timeout=timeout)
return r.get("result", {}).get("result", {}).get("value")
async def fire(self, expr):
"""执行 JS 但不等待 Promise(用于会阻塞的模态框调用)
关键:awaitPromise=False,否则 showConfirm/showPrompt
会阻塞 CDP 连接导致 ConcurrencyError
"""
return await self.evaluate(expr, await_promise=False)
async def screenshot(self, filename=None):
"""CDP 原生截图 — 无需 Xvfb/xfreerdp
直接通过 DevTools Protocol 的 Page.captureScreenshot,
返回 WebView 内容截图。比 RDP 截图更精确、更快。
"""
# 发送 CDP 命令,返回 base64 编码的 PNG
...
async def find_element(self, selector, description=None):
"""自愈选择器 — UI 重构时自动降级查找
如果主选择器失败,依次尝试:
1. ID 大小写模糊匹配
2. 文本内容匹配(按钮、链接、标题)
3. aria-label / placeholder 匹配
"""
found = await self.js(f"!!document.querySelector('{selector}')")
if found:
return selector
# 降级策略...
async def cdp_wait_modal(self, timeout_ms=5000):
"""等待模态框出现"""
return await self.js_json(
f"window.__omt_cdp.waitForModal({timeout_ms})"
f".then(m => JSON.stringify(m))"
)
async def cdp_respond(self, value):
"""响应模态框(替代人工点击确认/取消/输入)"""
await self.js(f"window.__omt_cdp.respond({json.dumps(value)})")E2E 测试运行器(含失败日志):
def run_tests(tests, group_name="G"):
async def _run():
setup_script_dir() # 创建临时目录
cdp = await connect()
for test_id, desc, fn in tests:
try:
await fn(cdp)
results[test_id] = "PASS"
except CDPReconnectNeeded:
cdp = await reconnect() # 页面导航后重连
results[test_id] = "PASS"
except SkipTest as e:
results[test_id] = "SKIP"
except Exception as e:
results[test_id] = f"FAIL: {e}"
# 失败日志 → JSONL 回归追踪
if failed > 0:
_log_failures(results, group_name)
cleanup_script_dir() # 清理临时目录
asyncio.run(_run())问题: 浏览器原生的 alert()/confirm()/prompt() 会弹出系统级模态对话框,完全阻塞 JS 执行线程和自动化测试。无论是 Selenium、Playwright 还是 CDP,处理这些对话框都很痛苦:
- CDP
Runtime.evaluate调用会被挂起直到对话框关闭 - 在 Tauri/Electron 等桌面应用中表现更不稳定
- 无法在自动化测试中可靠地检测和响应
方案: 将所有 alert/confirm/prompt 替换为自定义的 async 函数 + CDP Debug API:
// 替换前:
if (confirm("Delete this item?")) { deleteItem(id); }
// 替换后:
if (await app.showConfirm("Delete this item?")) { deleteItem(id); }showConfirm 在普通用户模式下渲染自定义 UI 弹窗,在测试模式下通过 CDP Debug API 暴露给自动化:
// Debug API(仅在 debug 模式下注入)
window.__omt_cdp = {
pending: null, // 当前等待响应的模态框
respond(value) { ... }, // 模拟用户点击
waitForModal(ms) { ... }, // 等待模态框出现
events: [], // 事件日志
state() { ... }, // 当前应用状态快照
};测试代码:
# 触发删除(showConfirm 会阻塞 —— 用 fire() 不等待 Promise)
await cdp.fire("app.deleteItem()")
# 等待模态框出现
modal = await cdp.cdp_wait_modal(5000)
assert modal["type"] == "confirm"
assert "Delete" in modal["message"]
# 模拟用户确认
await cdp.cdp_respond(True)关键细节: fire() 使用 awaitPromise=False,让 CDP 立刻返回而不等待 Promise resolve。否则 showConfirm() 会在等待用户响应时阻塞 CDP WebSocket,导致后续所有 evaluate 调用超时或触发 ConcurrencyError。
收益:
- 所有用户交互都可以在 E2E 中自动化
- 支持验证弹框内容(确认消息文案正确)
- 可以测试 "用户取消" 路径
- 普通用户完全无感(自定义 UI 体验更好)
问题: E2E 测试中经常需要知道应用的内部状态(登录了没有?当前在哪个页面?有几个会话?),但 DOM 查询只能看到 UI 表现,看不到应用状态。
方案: 在 debug 模式下注入状态查询 API:
window.__omt_cdp = {
state() {
return {
loggedIn: !!app._jwtToken,
currentTab: app._currentTab,
sessionCount: app._sessions.length,
isTauri: app._isTauri,
};
},
events: [], // 时序事件日志
waitForEvent(name, timeoutMs) {
// 等待特定事件发生(如 "session-created", "ws-connected")
}
};在关键操作点埋点:
// 会话创建
async createSession(opts) {
const session = await this._api.post('/sessions', opts);
this._cdpEmit('session-created', { id: session.id });
return session;
}
// WebSocket 连接
ws.onopen = () => {
this._cdpEmit('ws-connected', { sessionId });
};测试中等待异步事件完成:
# 创建会话后等待 WebSocket 真正连上
await cdp.js("app.createSession({type: 'bash'})")
event = await cdp.cdp_wait_event("ws-connected", timeout_ms=10000)
assert event["sessionId"] is not None相比轮询 DOM 的优势:
- 精确等待特定状态变化,不需要
sleep(3)猜时间 - 事件日志可以回溯调试("到底什么时候断开的?")
- 不依赖 UI 渲染时序
问题: 测试需要免密登录、API 直调、数据清理等能力,但这些能力如果泄漏到生产 = 安全事故。
方案: 通过启动参数控制,不是通过配置文件:
# 测试环境 —— 独立端口、独立数据库、显式启用测试认证
server --listen :7443 \
--db /test/data/sessions.db \
--test-token <random-token> \
--enable-test-auth
# 生产环境 —— 绝不出现 test 相关参数
server --listen :6443 \
--db /prod/data/sessions.db为什么用启动参数而非配置文件/环境变量:
- 配置文件可能被复制或同步
- 环境变量可能被
.env文件意外注入 - 启动参数在
ps aux中可见,更容易审计 docker-compose.yml里一眼能看到有没有 test 参数
问题: E2E 测试需要客户端进程持续运行,但 SSH 断连、进程崩溃都会导致客户端掉线,测试基础设施不稳定。
方案: Watchdog 循环 + 分离进程:
# 客户端守护(SSH 断连不死)
nohup bash -c 'while true; do
DANGER_ACCEPT_INVALID_CERTS=1 client terminal takeover
sleep 2
done' > /tmp/client.log 2>&1 &Desktop 应用在 SSH 中启动 GUI:
# 直接 SSH 执行 `start app.exe` 不行 —— 不在交互式桌面会话
# 用 schtasks /IT 解决:
schtasks /create /tn StartApp /tr "C:\path\start_app.bat" \
/sc once /st 00:00 /IT /f
schtasks /run /tn StartApp问题: 改了源码 → 跑了测试 → 声称 "修好了",但实际部署的 binary 可能是旧的(忘了 rebuild),或者 pre-commit hook 修改了源码。
方案:
# 验证 binary 包含修改
strings target/release/my-server | grep "expected-string"
# 验证线上版本
curl -sk https://server/api/v1/version | jq .project_version
# 在真实环境测试具体功能(不只是 health check)
# ❌ curl 返回 200 就收工
# ✅ 测试被修改的具体功能问题: UI 重构时元素 ID 改名、层级调整,所有 E2E 测试的 CSS 选择器集体失效。维护成本随测试数量线性增长。
方案: 选择器查找失败时自动降级到替代策略:
async def find_element(self, primary_selector, description=None):
"""主选择器失败 → 依次尝试替代策略"""
# 1. 主选择器(精确匹配)
if await self.js(f"!!document.querySelector('{primary_selector}')"):
return primary_selector
# 2. ID 大小写模糊匹配(处理 camelCase → kebab-case 重构)
if primary_selector.startswith("#"):
# 遍历所有 [id] 元素,case-insensitive 对比
...
# 3. 文本内容匹配(按钮文字变了但功能没变)
if description:
# 搜索 button, a, h1-h3, label, [role="button"]
...
# 4. aria-label / placeholder 匹配
if description:
# querySelector('[aria-label*="desc" i]')
...
return None # 所有策略都失败使用方式:
# 即使 #settingsView 被改名为 #settings-panel,测试依然能找到
selector = await cdp.find_element("#settingsView", description="settings")
if selector:
vis = await cdp.js(f"getComputedStyle(document.querySelector('{selector}')).display")收益:
- UI 重构时测试不会集体失效
- 降级查找有日志,方便事后更新选择器
- 遵循行业趋势:Playwright v1.56 的 Healer Agent 做的就是这件事
问题: 视觉验证需要截图,但 Xvfb + xfreerdp + import -window root 的方案链条长、延迟高、受 RDP 压缩影响。
方案: CDP 的 Page.captureScreenshot 直接通过 WebSocket 返回 WebView 内容:
async def screenshot(self, filename=None):
"""CDP 原生截图 — 直接获取 WebView 渲染结果"""
self._msg_id += 1
await self.ws.send(json.dumps({
"id": self._msg_id,
"method": "Page.captureScreenshot",
"params": {"format": "png"},
}))
# ... 等待响应,base64 解码 → PNG 文件优势:
- 不需要 Xvfb / xfreerdp(可以直接从远程 SSH 调用)
- 截图质量不受 RDP 压缩影响
- 速度更快(~100ms vs RDP 截图 ~2s)
- 可以截取 WebView 内容(不含窗口标题栏、桌面背景)
限制:
- 只能截取 WebView 内容,看不到原生窗口边框
- 某些 Tauri/Electron 应用可能需要先调用
Page.enable
问题: 测试失败了,修了代码,过了。但不知道历史上哪些测试最容易坏、什么时候坏过、错误模式是否重复。
方案: 每次测试失败自动追加 JSONL 日志:
def _log_failures(results, group_name):
log_path = os.path.join(SCRIPT_DIR, "failures.jsonl")
with open(log_path, "a") as f:
for test_id, result in results.items():
if result not in ("PASS", "SKIP"):
json.dump({
"group": group_name,
"test": test_id,
"error": str(result),
"ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
}, f)
f.write("\n")长期价值:
- 识别 flaky tests(同一个测试反复失败/通过)
- 发现回归模式("每次改 auth 模块,G14 就挂")
- 为 Eval-Driven Development(见下文)提供种子数据
1. 异步阻塞导致 CDP 死锁
自动化测试通过 CDP
Runtime.evaluate调用 JS 函数,如果该函数内部await了一个需要用户交互才能 resolve 的 Promise(如确认弹窗),CDP 连接就会被挂住。后续任何evaluate调用都会超时或报ConcurrencyError。
通用规则: 对可能阻塞等待用户输入的 JS 函数,使用 awaitPromise: false 发送 evaluate,让 CDP 立即返回。然后在另一次调用中检查/响应弹窗状态。
2. 页面导航导致 WebSocket 断开
测试中触发了页面跳转(如 OAuth 回调、URL 变更),CDP WebSocket 断开,后续调用全部失败。
通用规则: 封装 reconnect() 方法,在页面导航后自动重连。设计测试框架时用异常类型(如 CDPReconnectNeeded)区分预期的导航断开和真正的错误。
3. 跨测试状态传递
测试 A 创建了资源(会话、设备),测试 B 需要操作它。但每个测试函数是独立的。
通用规则: 提供 shared 字典在测试间传递状态。但尽量减少依赖——如果 A 失败了,依赖 A 的 B/C/D 应该 SKIP 而非报莫名其妙的错。
4. 代理干扰本地连接
系统配置了 HTTP 代理,
localhost:9222的 CDP 请求也被代理拦截 → 连接失败或返回 502。
通用规则: E2E 脚本启动时显式设置 no_proxy=localhost,127.0.0.1。对 Python 和 curl 都需要设置。
5. SSH 无法启动 GUI 应用
通过 SSH 远程执行
start app.exe,进程启动了但没有 GUI 窗口——因为 SSH 会话不在交互式桌面。
通用规则: Windows 上用 schtasks /IT 在交互式桌面会话中启动 GUI 进程。Linux 上用 DISPLAY=:99 + Xvfb。
6. 异步操作完成 ≠ UI 状态更新
API 调用返回成功,但前端缓存还是旧数据。E2E 测试读到的是缓存值而非最新值。
通用规则: E2E 验证优先查服务端 API(真实状态),而非前端 DOM(可能缓存)。或者在 Debug API 中提供 invalidateCache() 方法。
7. 测试产物污染应用目录
CDP 测试脚本、截图、临时 bat 文件直接放在应用目录下,积累 ~90 个文件。导致应用目录混乱,甚至影响应用启动。
通用规则: 所有测试产物(脚本、截图、临时文件)必须放在专用临时目录(%TMP%\omt-e2e / /tmp/omt-e2e),测试开始时创建、结束时自动清理。应用目录只保留应用本身的文件。
8. "接口通" ≠ "功能通"
health check 返回 200 就宣布部署成功,但实际的 WebSocket 中继是坏的——两个完全不同的代码路径。
通用规则: E2E 测试必须覆盖到具体功能的完整链路。如果改了会话创建,就测会话创建到输出可见的全流程。
9. 模态框内复杂交互的时序问题
测试流程:点击重命名按钮 → 等待 prompt 弹窗 → 输入新名称 → 确认。但 CDP 的
waitForModal和respond之间如果穿插了其他 CDP 调用,时序容易乱。
通用规则: 对复杂的模态框交互,把整个流程写在一段 JS 表达式里执行,而非多次 CDP 调用:
result = await cdp.js_json("""(async () => {
const cdp = window.__omt_cdp;
// 同时启动操作和等待弹窗
const modalPromise = cdp.waitForModal(10000);
const actionPromise = app.renameItem(id, oldName);
// 等弹窗出现
const modal = await modalPromise;
// 响应弹窗
cdp.respond("new-name");
// 等操作完成
await actionPromise;
// 在 JS 侧验证结果
const resp = await fetch('/api/items/' + id);
const data = await resp.json();
return JSON.stringify({ name: data.name, modalType: modal.type });
})()""")CDP 可以检查 DOM 状态,但无法验证:
- 元素是否真的可见(被遮挡、透明度为 0、overflow hidden)
- 布局是否正确渲染(CSS 错误导致元素重叠或错位)
- 整体 UI 是否符合预期(主题、配色、是否有视觉回归)
视觉验证是 DOM 断言的补充,而非替代。
方案 A(RDP 截图 — 看到完整桌面)
Xvfb :99 (虚拟显示)
└── xfreerdp (RDP 连接到 Windows 桌面)
└── import -window root screenshot.png (截图)
方案 B(CDP 原生截图 — 只看 WebView 内容,更快更精确)
CDPSession.screenshot("filename.png")
└── Page.captureScreenshot → base64 PNG → 文件
两种方案均可 → Ollama Vision API (本地推理) → 结构化 Yes/No 判断
我们使用的是量化后的 9B 参数 Vision 模型(如 Qwen2.5-VL 系列的 abliterated 变体),在消费级 GPU(RTX 3090/4090, 24GB VRAM)上可以流畅运行。
能做的事:
- ✅ 二分类问题:"这是不是登录页面?" → Yes/No
- ✅ 存在性检查:"页面上有没有侧边栏?" → Yes/No
- ✅ 主题检查:"这是深色主题还是浅色主题?"
- ✅ 粗粒度布局:"左边是导航,右边是内容区?"
做不到的事:
- ❌ 像素级比对(用 pixelmatch/resemble.js 更适合)
- ❌ 精确读取小字文本(9B 模型 OCR 能力有限,尤其是非英语文本)
- ❌ 复杂计数("页面上有几个按钮"——经常数错)
- ❌ 细粒度颜色判断("这个按钮是 #00ff41 还是 #00ff42"——无法区分)
- ❌ 稳定的长文本提取(偶尔会幻觉出不存在的文字)
核心原则:把问题设计成 Yes/No 二分类,用描述性语言而非精确引用。
# ✅ 好的提问 —— 二分类、描述性
"Is this a terminal application showing a dark-themed UI with a sidebar? Answer yes or no."
"Does this show a settings page with navigation tabs in a left panel? Answer yes or no."
"Is this a login page with username and password fields? Answer yes or no."
# ❌ 差的提问 —— 要求精确阅读、计数、对比
"What is the text of the third button from the left?" # OCR 不可靠
"Are there exactly 5 menu items in the sidebar?" # 计数不可靠
"Does the header say 'Oh My Term v2.1.3'?" # 精确文本匹配不可靠提问模板:
Is this [页面类型描述] with [关键视觉特征]? Answer yes or no.
Does this show [UI 组件描述] in [位置描述]? Answer yes or no.
强制格式约束: 末尾加 Answer yes or no. 或 Only answer yes or no. 约束输出格式。9B 模型如果不加约束,容易输出长段分析文本,增加后处理复杂度。
def vision_check(image_path, question):
"""调用本地 Ollama Vision 模型,返回 True/False"""
result = subprocess.run(
["python3", VISION_SCRIPT, image_path, question],
capture_output=True, text=True, timeout=60
)
answer = result.stdout.strip().lower()
return "yes" in answer
# 使用
screenshot = capture_rdp_screenshot() # Xvfb + import
assert vision_check(screenshot,
"Is this a dark-themed terminal with a session list on the left? "
"Answer yes or no."
), "Terminal view not showing correctly"# ❌ 默认分辨率可能太小
DISPLAY=:99 import -window root screenshot.png
# ✅ 确保 Xvfb 和 RDP 分辨率足够
Xvfb :99 -screen 0 1400x900x24 &
xfreerdp /v:$HOST /u:$USER /p:$PASS /w:1400 /h:900 /cert:ignore &
sleep 10 # 等 RDP 连接稳定
DISPLAY=:99 import -window root screenshot.png- 分辨率低于 1024x768 → 9B 模型识别率显著下降
- 截图时机很重要 → 页面加载/动画完成后再截
- RDP 压缩伪影 → 比本地截图质量差,尽量用 24-bit 色深
功能验证("能不能用") → CDP DOM 断言(精确、快速、确定性)
视觉验证("好不好看") → Ollama Vision(模糊匹配、慢、概率性)
回归检测("有没有变化") → 像素级 diff 工具(pixelmatch)
在 E2E 流程中,先跑 CDP 断言(99% 的测试用这个),最后跑 Vision(兜底检查整体视觉是否合理)。Vision 测试 SKIP 不应阻塞部署——它是预警,不是门禁。
-
验证闭环是铁律
改完代码 → 构建 → 部署 → 运行验证命令 → 贴输出。缺任何一环都不算完成。
-
先读再写
修改任何文件之前先读。代码即规范——grep 常量、路径、命名模式。盲改等于赌运气。
-
一次修一个,每次都验证
多个修复堆叠部署 = 出了问题不知道哪个改坏的。每个修复独立部署、独立验证。
-
画图再修并发 Bug
不要上来就改代码。先 grep 所有锁、所有共享状态访问点,画出依赖关系。根因通常不在报错的地方。
-
区分 "接口通" 和 "功能通"
HTTP 200 只代表服务活着。WebSocket 能连上不代表消息能正确中继。每层都要测到具体功能。
-
永远不要声称 "我无法解决"
你有工具:搜索引擎、源码阅读、多种调试手段。说 "无法解决" 之前,确保你:
- 读了错误信息的每个字
- 搜索了错误原文
- 读了报错位置上下 50 行代码
- 验证了所有假设(版本、路径、权限)
- 试过反向假设("如果问题不在我以为的地方呢?")
-
子 Agent 需要足够的上下文
spawn 子 agent 时,不要假设它知道任何上下文。必须在 prompt 里包含:
- 文件路径
- 具体要做什么
- 成功标准
- 相关的约束条件
-
用 TDD 指导开发
先写测试(哪怕是一个 curl 命令),确认它 fail,再写代码让它 pass。这个流程同样适用于 bug fix:先写能复现 bug 的测试。
-
日志和证据是你的盟友
strings binary | grep xxx验证编译产物curl -v看完整 HTTP 交互git log --oneline -5确认提交历史- 截图验证 UI 变更 这些不是可选项,是交付标准。
-
知道何时切换思路
同一个方向尝试 3 次还不行 = 方向错了。强制自己切换到本质不同的方案:
- 从 "调参数" 切到 "换算法"
- 从 "改代码" 切到 "改架构"
- 从 "修症状" 切到 "查根因"
┌─────────────────┬───────┬────────────────────────────────┐
│ 模块 │ 测试数│ 关键覆盖 │
├─────────────────┼───────┼────────────────────────────────┤
│ common (Rust) │ 54 │ 加密、版本、序列化、敏感字段 │
│ storage (Rust) │ 35 │ CRUD、审计日志、迁移 │
│ server (Rust) │ 101 │ 认证、JWT、限流、中继、集成 │
│ client (Rust) │ 81 │ 文件操作、PTY、WebSocket │
│ frontend (JS) │ 32 │ LZ4、protobuf、CDP API │
│ E2E (Python) │ 59 │ 9 组全链路测试 │
├─────────────────┼───────┼────────────────────────────────┤
│ 合计 │ 362 │ │
└─────────────────┴───────┴────────────────────────────────┘
基于对行业最新实践的调研,以下是与本文讨论的测试框架相关的趋势和经验。
Anthropic 工程博客提出的核心理念:像 TDD 对待代码一样对待 AI Agent 的评估。
- 在构建能力之前先定义成功标准(eval 先行)
- 从 20-50 个真实失败案例开始构建评估数据集
- 每次 bug fix = 新增一个 eval case,数据集随 agent 一起成长
- 对非确定性输出,每个任务跑多次 trial
- 三层评判:确定性检查(代码断言)、LLM-as-Judge(模型评分)、人工复核
与本文的关联: 我们的 JSONL 失败日志(Pattern 8)本质上就是 EDD 中的"从真实失败构建评估集"。差距在于还没有形成自动化的回归评估流水线。
两个生产级案例的共同结论:
OpenObserve 的 "Council of Sub Agents" — 8 个专项化 Claude Code agent(Analyst 分析需求、Sentinel 审计安全、Healer 修复 flaky test 等),每个 agent 有明确边界。效果:380→700+ tests,flaky tests 减少 85%,分析时间从 45min 降到 5min。关键教训:早期用单个 "super agent" 的尝试失败了,专项化才是关键。
Metropolis 的多 Agent E2E 生成器 — 坦诚的教训:他们花在调优 prompt 上的时间比人工写测试还多。每次运行都暴露新的必要调整。测试创建的并行化在实践中不可行。代码库特定上下文是测试质量最大的驱动因素。
Playwright 在 v1.56 引入了三个专项化 Agent,专为 AI 辅助测试设计:
| Agent | 职责 | 对应本文能力 |
|---|---|---|
| Planner | 探索应用,生成 Markdown 测试计划 | cdp_state() + waitForEvent() 的可观测性 |
| Generator | 将计划转为可执行的 Playwright 测试 | 我们的 cdp_group_*.py |
| Healer | 重跑失败测试,自动修复选择器 | 我们的 find_element() 自愈选择器 |
这三个 Agent 通过 MCP(Model Context Protocol)与 Claude Code / VS Code Copilot 集成。
与本文的关联: 我们的 find_element() 自愈选择器做的和 Healer Agent 类似——在主选择器失败时自动降级到替代策略。差别在于 Playwright 的方案更成熟,支持截图比对和 AI 重定位。
行业中越来越多的团队采用这种模式减少"维护税":
- 缓存上一次成功运行时的选择器(known-good locators)
- 选择器失效时,降级到 AI 驱动的元素识别
- 找到元素后自动更新缓存,后续运行恢复确定性
核心度量: 一个工具如果创建省了 10 小时但维护多花了 20 小时 = 净亏损。维护税比创建成本更重要。
| 层级 | 评估方法 | 适用场景 | 工具 |
|---|---|---|---|
| 工具调用级 | 确定性断言 | 单个 API 返回值对不对 | pytest, vitest |
| 轨迹级(Trace) | LLM-as-Judge 评分 | Agent 的推理路径合不合理 | Langfuse, DeepEval |
| 会话级 | 端到端成功率 | 整个任务完成了没有 | 自定义 runner |
| 生产级 | 持续监控 + 回归检测 | 线上有没有退化 | Arize Phoenix, Braintrust |
关键洞见: 会话级成功可以掩盖工具级问题——"测试通过了但推理路径有隐患"。需要在多个层级同时评估才能得到完整图景。
我们的 Ollama Vision 验证(第五节)本质上就是 LLM-as-Judge 模式的视觉变体:
- 用 AI 模型对另一个系统的输出做判断
- 输出格式化为结构化的 Yes/No(降低后处理复杂度)
- 非确定性,需要统计多次结果
行业趋势是将这种模式从视觉扩展到全部评估维度:代码质量、推理完整性、安全合规性。
- Demystifying evals for AI agents — Anthropic Engineering — Agent 评估方法论
- How AI Agents Automated Our QA: 700+ Test Coverage — OpenObserve — 8 Agent 协作 QA 实践
- Beyond the hype: Building a multi-agent system for E2E test generation — Metropolis — 多 Agent E2E 生成的真实教训
- Eval-driven development: Build and evaluate reliable AI agents — Red Hat — EDD 实践指南
- Playwright Agents: Planner, Generator, and Healer — DEV Community — Playwright v1.56 三 Agent 架构
- Agent Factory: Top 5 agent observability best practices — Microsoft Azure — Agent 可观测性
- Complete Guide to E2E Testing in 2026 — Shiplight AI — E2E 测试综述
- Claude Code with Playwright: 4-agent test generation pipeline — TestDino — Claude Code + Playwright 集成
- Evaluating AI agents: Real-world lessons from Amazon — AWS — Amazon 的 Agent 评估经验
- Evaluating AI Agents in Practice: Benchmarks, Frameworks, and Lessons Learned — InfoQ — Agent 评估框架对比
2026-04 实践总结。私有信息已替换为占位符。