Skip to content

Test Detection 与 Failure Parser

本课交付结果

你将交付 detectTestCommand(files)parseTestFailure(output):按 pnpm、npm、pytest 优先级选择测试,并识别 typecheck、assertion、dependency、timeout 与 unknown。

岗位问题

Agent 若猜错测试命令,会把环境问题误判成代码问题;若只把整段日志喂回模型,重试策略无法区分可修复断言、缺依赖与超时。

前置检查

前置知识快照

先验证执行器:pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 13。检测只产生结构化命令,不直接运行;解析器必须限制摘要长度。

检测与执行必须分开。detectTestCommand 只根据可信文件清单提出 executable/args,仍要经过 Command Guard、审批与 Executor;它不能因为发现锁文件就直接启动包管理器。parseTestFailure 只解释已经受限的输出,不负责重跑或修改代码。

标志文件是仓库事实:pnpm-lock.yaml 强烈表明 pnpm,package-lock.json 指向 npm,pyproject.tomlpytest.ini 指向 Python 测试。文件遍历顺序不应影响结果,所以先构造 Set,再按明确优先级判断。混合仓库的选择是产品规则,而不是偶然先看到哪个文件。

原理拆解

锁文件比泛化的 package.json 更能指示包管理器,因此 pnpm 优先;JavaScript 标志优先于 Python。失败解析按具体模式到通用模式匹配,最后返回 bounded unknown,永不因陌生日志崩溃。

mermaid
flowchart LR
  A[工作区文件清单] --> B{pnpm 锁文件?}
  B -- 是 --> C[pnpm test]
  B -- 否 --> D{npm 标志?}
  D -- 是 --> E[npm test]
  D -- 否 --> F{pytest 标志?}
  F -- 是 --> G[pytest]
  F -- 否 --> H[未发现]
  I[受限 stdout + stderr] --> J[具体模式优先匹配]
  J --> K[typecheck/assertion/dependency/timeout]
  J --> L[bounded unknown]

为什么 pnpm 优先于 package.json?每个 JavaScript 项目几乎都有 package.json,仅凭它不能判断执行 npm;锁文件表达实际包管理器选择。若同时存在多个锁文件,课程用固定优先级保持确定性,生产系统则应报告冲突或读取 packageManager 字段,避免默默选择错误工具。

检测结果保持结构化。返回 {executable:"pnpm", args:["test"]},而不是字符串 pnpm test;后续 Guard 能识别程序和参数,Executor 无需 shell。任何自动发现都不能破坏前两课建立的命令边界。

失败 taxonomy 的价值是驱动不同恢复:typecheck 通常修改类型或实现,assertion 需要对比预期与实际,dependency 可能需要用户批准安装,timeout 需要缩小范围或检查死循环,unknown 需要保留摘要并谨慎停下。若所有失败只是一段文本,Agent 会用同一种“再试一次”处理不同问题。

模式顺序从具体到通用。error TS2322 明确属于 typecheck,AssertionError 属于断言;若先用宽泛 error 匹配,几乎所有日志都会误分类。每个正则都应有近似反例,证明普通上下文单词不会触发。

unknown 是合法结果,不是解析器异常。框架版本、语言与插件输出不断变化,分类器不可能穷尽;稳定 unknown 让循环保持可观察,团队可根据真实日志增加规则。若陌生日志 throw,一次普通框架升级就会击穿整个 Agent。

摘要先 trim 并压缩空白,再截到固定长度,避免几千行日志进入模型。分类必须对原始输出运行,因为压缩可能改变行结构或丢失精确信号;摘要只是展示与恢复提示,不替代受控 Trace 中的失败指纹。

代码实验

失败实现

下面实现依赖文件顺序,并把任何 error 当成同一类别:

ts
function unsafe(files: string[], output: string) {
  const first = files.find((file) => file.includes("package"));
  return { command: first ? "npm test" : "pytest", kind: /error/i.test(output) ? "assertion" : "unknown" };
}

正确检测使用明确集合和优先级,解析器返回有界结构:

ts
export function detectTestCommand(files: readonly string[]): TestCommand | undefined {
  const set = new Set(files);
  if (set.has("pnpm-lock.yaml")) return { executable: "pnpm", args: ["test"] };
  if (set.has("package-lock.json") || set.has("package.json")) {
    return { executable: "npm", args: ["test"] };
  }
  if (set.has("pyproject.toml") || set.has("pytest.ini")) {
    return { executable: "pytest", args: [] };
  }
  return undefined;
}

export function parseTestFailure(output: string): TestFailure {
  const summary = output.trim().replace(/\s+/g, " ").slice(0, 200);
  if (/error TS\d+|TypeScript/i.test(output)) return { kind: "typecheck", summary };
  if (/AssertionError|expected .+ (?:to be|to equal)/i.test(output)) return { kind: "assertion", summary };
  if (/Cannot find module|ModuleNotFoundError|ERR_MODULE_NOT_FOUND/i.test(output)) return { kind: "dependency", summary };
  if (/timed out|timeout/i.test(output)) return { kind: "timeout", summary };
  return { kind: "unknown", summary };
}

打开本课 Lab

bash
pnpm --dir bootcamps/coding-agent/labs/14-test-detection/starter test
pnpm --dir bootcamps/coding-agent/labs/14-test-detection/solution test

Starter 声明 实现测试失败解析;Solution 应通过 5 项测试。

测试要把混合仓库和未知长日志都写成边界:

ts
it("混合仓库按固定优先级选择 pnpm", () => {
  expect(detectTestCommand(["pyproject.toml", "package.json", "pnpm-lock.yaml"]))
    .toEqual({ executable: "pnpm", args: ["test"] });
});

it("未知日志始终返回有界摘要", () => {
  const result = parseTestFailure("mystery\n" + "x".repeat(500));
  expect(result.kind).toBe("unknown");
  expect(result.summary.length).toBeLessThanOrEqual(200);
});

关键实现讲解

检测结果仍是 executable/args,不生成 shell 字符串。解析前可压缩空白,但分类应使用原始输出;摘要截到 200 字符,避免未知日志无界进入上下文。

总项目可把 stdout 与 stderr 合并为受限解析视图,同时保留来源元数据。类型错误常在 stderr,测试框架有时把断言写 stdout;只解析一条流会漏判。合并仍要服从 Executor 的总预算,Parser 不应重新读取磁盘上的无限日志。

分类结果最好附带 fingerprint,例如规范化首个错误、文件位置与类别的哈希。连续重试得到相同 fingerprint,循环可判断没有进展并停止;日志时间戳或耗时变化不应制造新失败身份。课程先实现 kind 与 summary,为后续恢复循环建立最小接口。

运行与验证

真实运行输出

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 14
pnpm --filter @coding-agent/runner test

混合仓库应选择 pnpm;TS2322、AssertionError、Cannot find module 和 timed out 分别落入预期类别。

真实 Lab 中 Starter 五项全部失败,Solution 五项全部通过:

text
starter: 5 failed, 0 passed
solution: 5 passed, 0 failed
覆盖: 命令优先级 · TypeScript · assertion · dependency/timeout · bounded unknown

再打乱文件清单顺序多次,结果必须一致;给每种关键词构造近似反例,确认宽泛普通文本不会误触;空输出应稳定返回 unknown 和空摘要,而不是抛错。

常见失败与排查

故障案例 1

pnpm 仓库被错误执行 npm。

症状:测试命令找不到依赖或生成第二份锁文件,Agent误判仓库损坏。

根因:检测看到 package.json 就立即返回 npm,没有先检查更具体锁文件。

定位:用同时含 package.json、pnpm 锁文件和 Python 配置的清单测试,打乱顺序比较结果。

修复:集合化文件清单,按明确优先级匹配;生产环境检测多锁冲突并给出诊断。

故障案例 2

所有日志都被分类为类型错误。

症状:缺依赖和断言失败也触发修改 TypeScript 的恢复策略,反复生成无关补丁。

根因:正则只搜索宽泛的 error 或 TypeScript 文件扩展名,规则顺序过宽。

定位:为每类准备正例和带普通 error 单词的反例,输出首个命中规则身份。

修复:具体框架信号优先,宽规则后置或删除;用评测混淆矩阵观察误报。

故障案例 3

陌生日志吞掉上下文或击穿循环。

症状:新测试框架输出无法识别时,Parser throw 或把数万字符原样发送给模型。

根因:把 unknown 当程序缺陷,且摘要没有硬长度上限。

定位:输入随机长文本和空文本,检查函数是否始终返回、summary 是否有界。

修复:unknown 作为稳定兜底,摘要规范化并截断;完整日志仍受 Executor 预算与受控存储约束。

课后作业

加入 cargo、go test 和 monorepo workspace 检测;为 Vitest/Jest/Pytest 提取首个失败文件和行号,并保留原始失败指纹。

进阶要求是把检测规则声明成带置信度和证据的候选列表,不在多根仓库里擅自只选一个。Parser 增加框架适配器与 fingerprint,使用固定语料评测准确率、unknown 比例和摘要泄密,证明新增规则不会压过更具体旧规则。

验收 Rubric

维度通过标准常见扣分
检测标志文件优先级确定依赖目录遍历顺序
分类五类结果稳定所有错误归为 unknown
摘要归一且有长度上限原样塞入完整日志

总项目增量

总项目包:@coding-agent/runner

总项目路径:packages/runner/src/test-detection.tspackages/runner/src/failure-parser.ts

总项目验证命令:pnpm --filter @coding-agent/runner test

下一课组合策略、Guard 与 Executor,形成 Week 3 的应用层 Sandbox Runtime。

延伸阅读

方案对比与工程取舍

硬编码标志文件优先级简单、确定、离线可测,适合少量主流生态;读取 package scripts 更精确,却可能遇到任意脚本与 workspace;直接让模型猜命令灵活,但结果不可重复且可能生成危险参数。可靠系统先用确定规则提出候选,再把不确定性展示给用户或规划层,最终仍经 Guard。

单正则分类器实现轻量,却随着框架增多形成冲突;按框架选择专用 parser 能提取文件、行号、测试名和 expected/actual,前提是先可靠识别框架;机器学习分类可处理长尾,却难解释且需要语料。常见架构是确定适配器优先、通用模式其次、unknown 兜底。

混合仓库不应永远只执行一个根命令。monorepo 可能同时包含前端 pnpm、后端 pytest 和 Rust cargo;课程固定优先级用于最小 Lab,生产检测应识别 workspace 根与受影响文件,生成多个候选命令。选择依据进入 Trace,Agent 可以先跑最相关子集,再跑全量检查。

锁文件冲突是诊断信号。仓库同时含 pnpm 与 npm 锁文件,可能是迁移残留、嵌套项目或错误提交。默默选一个能保持确定,却可能产生破坏性安装。检测器可返回 ambiguous 候选与证据,由用户或项目配置确认;只读测试仍可在已安装环境中尝试最可信命令。

packageManager 字段、workspace 配置和 CI 文件都能提高置信度。证据应按可信程度排序,项目内 AGENTS 指令可以推荐命令,但不应绕过 Command Guard。模型从 README 读到的自由文本属于低置信建议,必须结构化并审批后执行。

受影响测试选择能降低延迟,却可能漏掉跨包回归。策略可以先运行目标包或文件测试,补丁形成后再跑 workspace 全量门禁。检测器返回层级化命令:快速、相关、完整;Agent 根据阶段和预算选择,而不是只拥有唯一 test

Parser 的顺序要考虑一段日志可能同时含多类信号。缺依赖导致测试框架最终打印 assertion summary,真正根因仍是 dependency;编译失败可能包含 expected 字样。规则优先级应由恢复价值决定,并用真实复合日志测试,不是随代码书写顺序偶然决定。

首个失败不一定是根因。并行测试输出会交错,后续 setup error 可能更关键。框架适配器可提取所有有限失败,再按依赖、编译、断言与超时的因果启发式排序。课程只返回一类,生产结果可以带 primary 与其他 observedKinds,仍保持摘要有界。

fingerprint 需要移除时间戳、绝对临时路径、随机端口和耗时,再保留错误类别、相对文件、行号、测试名与核心消息。过细会让同一失败每轮看似不同,过粗会把多个断言混成一个。用历史重试日志验证稳定性,是比主观设计更好的方法。

摘要不能在 UTF-16 中间随意切割,也应避免泄露密钥。先对受限输出做脱敏与 ANSI 清理,再按字符或 token 安全截断,标记是否截短。Parser 自身不应把“看起来像 token”的内容复制到 Trace;只保存受控 fingerprint 和必要定位。

TypeScript 规则可匹配 error TS 加数字,而不是单词 TypeScript;断言规则针对框架固定短语;依赖规则覆盖 Node 与 Python 模块缺失;超时规则区分测试超时与普通文案“timeout setting”。每条规则都需要真实正例、近似反例和复合日志。

unknown 比例是一项质量指标,不是必须归零的坏事。强行覆盖所有 unknown 往往增加误分类,而误分类会驱动错误自动修复。保持高精度、适度 unknown,让 Agent展示摘要或请求帮助,比自信地给出错误类别更安全。

恢复策略应由类别映射但保留预算。typecheck 可读取定位文件后修改,assertion 先比较期望,dependency 需要审批安装或报告环境,timeout 不应原样无限重试,unknown 最多尝试一次更详细诊断。每类都有重试上限与停止条件。

命令检测失败返回 undefined 也不是异常。没有已知标志时,Agent可查看项目指令或询问用户,而不是默认运行某个全球命令。默认 npm test 在非 JavaScript 仓库既无效又浪费时间。无证据时诚实表达未知,是确定系统的重要能力。

文件清单必须来自安全列表工具,使用规范化相对路径并限制深度。检测器不直接递归文件系统,避免重复实现 symlink 与资源边界。嵌套 workspace 规则可消费带目录层级的清单,但不接受模型伪造的绝对路径。

检测结果也要绑定 cwd。根 pnpm test 与子包 npm test 不是同一动作,Guard 审批和 Executor执行必须使用同一个可信目录。结构可扩展为 {executable,args,cwdRelative,evidence},Runtime 将相对 cwd 解析在工作区内。

测试语料应版本化。每次新增框架日志,记录来源、期望类别、定位字段与脱敏后的最小文本;规则变更跑整套混淆矩阵。只用手写单行字符串会错过颜色码、并行前缀、多行堆栈和框架摘要冲突。

端到端验收从临时混合仓库开始:安全列文件,检测 pnpm,Guard 允许测试,Executor用干净环境与预算运行失败夹具,Parser 返回 assertion 与有界摘要。各组件单独正确不代表组合字段一致,这条链证明 Week 3 到当前为止能把命令事实转成可恢复观察。

用课程的五个断言具体理解解析行为。输入 error TS2322: Type string is not assignable,第一条规则提取 typecheck;输入 AssertionError: expected 1 to be 2,断言规则命中;Cannot find module 'x' 进入 dependency;Test timed out in 5000ms 进入 timeout;无法识别的五百字符文本返回 unknown,摘要最多二百字符。每条结果都是稳定数据,而非依赖模型临场阅读。

分类前不要只看进程退出码。不同测试工具都常用 code 一表示失败,code 二可能是配置错误或中断,单个数字不足以决定恢复。Parser消费受限文本与执行终态;如果 timedOut flag 已经明确为真,可以由调用层直接产生 timeout,不必依赖日志恰好包含英文关键词。结构化信号优先于自由文本。

同样,dependency 不能自动触发安装。模块缺失可能是错误 import、workspace 构建未运行、可选依赖或环境未准备。Parser只报告类别与证据,规划层检查清单和锁文件,再通过 ask 审批安装。把分类直接连到副作用会把一个误报放大成供应链操作。

断言失败需要保留 expected/actual,但要限制尺寸。快照测试可能输出数万行差异;框架适配器提取首个断言、相对文件和有限 diff,完整日志继续留在受控临时文件。给模型足够定位的最小证据,比把所有差异塞进上下文更有效。

类型错误常成批出现,首个错误可能导致后续级联。Parser可以统计总数并只返回前若干独立文件,Agent先修最早根因再重跑。若每轮把数百条错误全部发送,预算被噪声占满,也难判断修复是否减少问题。

超时类别要结合 Executor 事实。日志中“timeout config is 5000”可能只是配置打印,不代表任务超时;Executor 的 timedOut:true 是强证据。框架主动报告单个测试超时但进程正常退出,则文本规则仍有价值。结果可区分 process_timeout 与 test_timeout,恢复策略分别调整命令预算或检查测试逻辑。

unknown 摘要为空时,可能是进程没有输出、被 signal 杀死或执行器配置问题。上层应结合 exitCode、signal、truncated 和 spawnError,不要只让模型猜。Parser是观察的一部分,不是唯一事实来源。设计组合结果时保留原始结构化执行元数据。

输出被截断会影响分类:关键总结可能在尾部。Executor可保留有限头尾,Parser在两段都运行,并在结果标注 incomplete:true。若仍 unknown,Agent可用更具体测试或调整可信预算,而不是默认认定没有错误。截断信号必须穿过所有层。

ANSI 颜色码会插入到关键词中,例如 error 的字母之间夹控制序列,正则可能漏判。解析前制作一个受限清理视图,去除 ANSI 和回车进度更新;分类仍不应执行任意终端控制。保留原日志哈希,清理文本用于匹配和摘要。

绝对路径规范化既保护隐私,也稳定 fingerprint。临时目录每次不同,如果原样哈希,连续相同失败看起来全新。把工作区内路径转成相对路径,工作区外路径替换为占位符;不要让 Parser自行访问文件系统,路径映射由可信 Context 提供。

monorepo 中“优先 pnpm”只决定根候选,不代表每个包都跑根全量测试。受影响文件属于 apps/web 时,可读取 workspace 元数据生成 pnpm --filter web test,但 --filter 值必须来自解析后的项目清单而非模型自由输入。快速命令之后仍安排根门禁,防止跨包契约回归。

Python 检测也不能只看到 pyproject 就假定 pytest;项目可能使用 unittest、tox 或 nox。读取受限配置和 CI 命令可以提高证据,无法确认时返回多个 ask 候选。课程选择 pytest 是可重复基线,文章要明确简化边界,避免学生把规则当普遍真理。

Rust 和 Go 扩展同理:Cargo.toml 可建议 cargo test,go.mod 可建议 go test ./...,但依赖下载、构建脚本和工作区范围仍受 Guard 与 Executor。新增生态是增加检测适配器,不是给模型 shell 自由。

规则准确率可以用混淆矩阵衡量:每类真正日志被预测成各类别的数量。dependency 误报成 assertion 会导致错误修复,timeout 误报 unknown 会降低恢复效率。根据业务代价设置精度优先级,尤其自动动作相关类别宁可 unknown 也不要误报。

解析器升级可 shadow 运行:线上仍使用旧类别,同时记录新解析器结果并比较,不驱动动作。积累足够样本后审查差异再切换。对安全相关自动恢复,这比直接发布新正则更稳妥,也能发现特定框架版本的意外冲突。

用户反馈可以纠正类别,但不能直接把单次文本写成宽泛全局规则。保存脱敏最小夹具,由开发者添加精确模式与反例,经过整套语料验证。自动从日志学习规则会被恶意仓库输出污染,形成持久绕过或错误分类。

最后做停止条件测试:连续两轮得到相同 fingerprint 且补丁无变化,Agent应停止并展示失败;类别从 typecheck 变 assertion 说明有进展,可以继续;变成 dependency 则需要审批而非继续改代码。稳定 taxonomy 的最终价值,是让循环有证据地选择下一步与停止。

验收报告应同时展示检测证据与解析证据:为何选择某个测试命令、命中了哪条失败规则、摘要是否截断、执行输出是否完整。只给最终 kind 会让误判难以追踪。人可以从证据重新判断,机器则使用稳定字段驱动流程,这正是可解释自动化与纯文本猜测的区别。

还要保存未识别样本的匿名统计:框架线索、输出长度、退出状态与频率,而不是完整私有日志。高频 unknown 值得新增适配器,单次长尾保留人工解释。工程资源应优先降低真实任务中的不确定性,而不是追求理论上覆盖所有字符串。

在发布新检测器前,随机打乱文件清单、重复解析同一日志、改变临时绝对路径和耗时数字,结果命令与 fingerprint 都应保持预期稳定。再替换真正错误核心,fingerprint 必须变化。这组变形测试比几个关键词正例更能证明系统抓住了语义而非偶然文本。

最后明确数据边界:文件发现来自安全工作区,命令经过 Guard,输出来自有界 Executor,Parser返回有界摘要。任何调用方如果跳过其中一环,失败分类即使看起来准确,也不能证明执行安全。下一课会把这条链固定为不可绕过的 Runtime。

分类准确只是手段,稳定而正确的恢复才是最终验收标准。

下一课衔接

现在权限策略、Command Guard、Process Executor、Test Detection 与 Failure Parser 都已存在,但调用方仍可能跳过其中一步。下一课实现 Sandbox Runtime 作为唯一 Enforcement Point:先分类,ask/deny 零执行,只有 allow 才固定 cwd 与 shell:false 调用 Executor,并在结果中保留决定与执行因果链。

  • Error taxonomy 与 retry policy。
  • Monorepo 测试发现策略。
  • 日志 fingerprint 与失败去重。

从零实现 Mini Code Agent Runtime