Skip to content

端到端 Bug Fix

本课交付结果

你将交付 runBugFixAgent(fixture, script):在临时副本严格执行 search → read → edit → test → diff → finish;test 未通过或 diff 为空禁止 finish,返回变更文件与事件证据,源夹具前后树哈希一致。

岗位问题

单元能力全绿不代表组合正确。Agent 可能没读就写、测试失败仍 finish、在源 fixture 上直接修改,或报告了不存在的 diff。端到端门禁必须验证行为顺序和外部状态,而非只看最终字符串。

前置检查

前置知识快照

先完成第 29 课夹具和第 17 课编辑事务:

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 29
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 17

Lab fixture 的 bug 是 add 错写成 a - b,verification 要求出现 a + b

第 29 课保证每轮评测得到全新 fixture 副本,第 17 课保证编辑事务不会留下半状态。本课验证它们与 search、read、test、diff、finish 组合后仍保持正确。端到端测试关注用户可观察行为和外部状态,不通过 mock 内部函数调用数量替代真正文件变化。

确定性 action script 是 Fake Provider 的等价教学夹具:它固定模型决策,让失败可以归因 Runtime 组合,而不是网络或模型随机性。脚本不是要求真实 Agent 永远只走六步,而是定义一条最小能力场景和完成协议;换真实 Provider 后仍保留相同黑盒断言。

源 fixture 是基准事实,run 只能获得 temporary workspace。开始前按排序相对路径与字节计算 sourceHash,结束再次计算;成功/失败都删除临时根。测试读取原始 Buffer 前后相等,防止第一次修复污染第二次评测。

原理拆解

mermaid
flowchart LR
  A[复制全新 Fixture] --> B[search 定位候选]
  B --> C[read 获取精确证据]
  C --> D[edit 唯一匹配事务]
  D --> E[test 独立验证]
  E --> F[diff 计算真实变更]
  F --> G{test 通过且 Diff 非空?}
  G -->|是| H[finish completed]
  G -->|否| I[拒绝完成]
  H --> J[复核源 hash 并清理]
  I --> J

源目录先计算路径+字节 SHA-256,再复制到临时 workspace。六步按固定协议执行:搜索要有命中、读取要安全、编辑目标唯一、test 读取独立 verification、diff 比较基线 bytes、finish 要求 testPassed 与 changedFiles 非空。

代码实验

打开本课 Lab

bash
pnpm --dir bootcamps/coding-agent/labs/38-end-to-end-bug-fix/starter test
pnpm --dir bootcamps/coding-agent/labs/38-end-to-end-bug-fix/solution test

Starter 应声明 实现端到端验证门禁;Solution 应通过 5 项临时夹具测试。

失败实现

ts
async function fixUnsafe(fixture: string, answer: string) {
  await writeFile(join(fixture, "src/math.ts"), answer);
  return { status: "completed", changedFiles: ["src/math.ts"] };
}

失败实现直接改源,根据模型声明填写 changedFiles,没有 search/read、路径、测试或 Diff 证据;任何 answer 都被标 completed。单次展示可能看起来修好了,第二次基准已经被污染,乘法或删除文件同样会“成功”。

完成门禁只消费运行事实:

ts
if (action.type === "test") {
  await runIndependentVerification(workspace);
  testPassed = true;
}
if (action.type === "diff") {
  changedFiles = await diffTrees(baseline, await files(workspace));
}
if (action.type === "finish" && (!testPassed || changedFiles.length === 0)) {
  throw new Error("finish 前必须通过验证并产生 Diff");
}

关键实现讲解

动作脚本是确定性模型 fixture,不是硬编码真实 Agent 的唯一策略;它用于验证 Runtime 组合。错误 edit 改成 a * b 时 test 必须失败,后续 diff/finish 不再执行。finally 总会清理副本。

fixture 设计要小而真实:源码、失败测试或 verification、包配置和任务描述都进入版本控制,bug 只有一个明确目标。过度简化成单字符串替换可教流程,却不能覆盖真实 TestRunner;总项目 examples/js-bug-fix 使用 Node test 与 Git diff,形成第二层集成证据。

source tree hash 包含规范相对路径和文件字节,目录枚举排序;生产还可纳入权限。不能只 hash math.ts,因为 Agent 可能污染 task.json 或新增文件。hash 前后相同证明最终原态,配合只读 source/独立 copy 预防运行期间写入。临时目录路径不进入报告 hash。

复制完成后对 workspace 建 baseline Map 或初始化本地 Git commit。Map 适合小 Lab,无外部依赖;Git 能给真实 rename、binary、mode 与 patch,但需隔离 config/hooks。无论方式,changedFiles 从 baseline/current 计算,不接受模型传入列表。

路径解析对 read/edit/verification 全部使用相同 Workspace Boundary:拒绝绝对、点点段,resolve 后 relative 仍在 root,真实文件处理链接。不能只保护 edit 而允许 read 访问外部 secret;test task.json 内的 verification.path 也是数据,同样不可信。

search 是证据收集的第一步。query 非空、结果有上限且至少一个匹配;返回相对路径、行范围和安全 snippet。Lab 对所有文件字节查 substring,生产 SearchCodeTool 使用受控 glob/rg、超时和输出预算。搜索零命中时不能继续猜 edit 路径。

read 证明 Agent 看过目标当前内容。动作协议可要求 edit path 曾被 read,read hash 与编辑前文件 hash 相等;否则搜索后文件变化或模型使用旧上下文会误改。课程固定顺序但可进一步记录 evidence,真实 Runtime 的工具 Trace 能验证这条因果链。

edit 使用 oldText 唯一匹配而不是自由覆盖整文件。零匹配说明上下文过期,一以上说明定位不明确;两者都停止。写入通过临时文件/rename 或 SearchReplaceEditor 事务,保留 mode,写后验证预期 hash。newText 有字节上限且不能把文件变为无效 UTF-8(若文本工具)。

编辑不等于修复。verification 必须独立于模型:Lab task.json 要求目标包含 a + b,总项目真实运行 node --test。更强测试先证明基线失败、修复后通过;若基线本来绿,任务可能已污染或测试没覆盖。命令由受控 fixture/Runner 选择,模型不能改成 echo passed

测试运行在 sandbox,shell false、cwd workspace、timeout/output cap、环境最小化。Agent 允许修改源码,但测试清单/隐藏 verification 是否可写取决于基准;通常 grader 位于 workspace 外或 hash 保护,防 Agent 直接改测试期待。测试失败保留安全摘要并阻止 finish。

diff 在测试后计算,以确保最终测试对应当前变更。如果 diff 后又 edit,之前证据失效,状态机把 testPassed/diffReviewed 重置;课程固定六步天然避免。真实 Agent Loop 每次写工具调用后应标验证过期,必须重新 test/diff 才允许 finish。

changedFiles 集合取 baseline/current key 并集,比较存在性和 bytes,排序去重;这样新增、修改、删除都可见。Buffer.alloc(0) 作为缺文件哨兵要小心空文件与不存在区别,显式 has 更清晰。Git diff 还要检查路径越界、二进制、权限和允许路径。

finish 不是普通模型消息,而是 Runtime 状态转移。需要最后一次相关写入之后 test 通过、Diff 非空且已审查、无 pending approval、预算未超、取消未触发。模型 summary 只描述结果,不能覆盖门禁。失败状态保持证据并返回下一行动,不伪装 completed。

ts
it("rejects false completion after a wrong edit", async () => {
  const bad = goodScript.map((action) => ({ ...action }));
  bad[2] = { type: "edit", path: "src/math.ts",
    oldText: "a - b", newText: "a * b" };
  await expect(runBugFixAgent(root, bad)).rejects.toThrow("验证失败");
  expect(await readFile(join(root, "src/math.ts"), "utf8")).toContain("a - b");
});

错误顺序测试不只传 edit/finish 短数组,还可交换 read/edit、diff/test、在 finish 后追加动作,全部在 copy/执行前验证脚本 schema。真实策略不固定一条序列,但状态机仍禁止未读写、写后未测和空 Diff finish;用模型 action fuzz 验证非法转移。

events 是审计证据而非简单字符串。Lab 返回六种 type,项目 Trace 记录 schemaVersion、runId、sequence、timestamp、action input hash、result摘要、usage 与副作用 fingerprint。错误路径也保留到失败点;全文参数经过脱敏,secret 不因 e2e 测试落盘。

run result 返回 status、summary、steps、toolCalls、changedFiles/sourceHash/workspaceHash、test evidence、diff hash 和 Trace link。每项可由外部状态复核。只断言 completed 会漏掉做错顺序或假测试;端到端断言应覆盖状态、证据链、最终代码和源原态。

finally 清理 temporary 不应覆盖原业务错误。先捕获 operation error,复核 source hash,导出受控 Trace/Diff,删除临时根;污染 source 是高优先错误,cleanup failure 作为另一个 cause/告警。测试成功失败都检查 workspace 不存在。

失败 artifact 有助排查,但不能保留整个 workspace 默认永久。导出 Diff、测试摘要、Trace 与 manifest hash到受控目录,设置大小/TTL/秘密扫描,然后清理副本。需要人工复现时通过 fixture hash和 action script重新生成,而不是依赖脏临时目录。

重复运行同 fixture/script 应得到相同事件类型、changedFiles、sourceHash和测试结果;timestamp/temp path不进入语义 hash。Fake Provider 让模型动作确定,工具命令和依赖也锁版本。若结果漂移,查环境、文件排序、测试随机与全局 Git 配置。

真实 E2E 用 FakeModelProvider 依次输出 search_code、read_file、edit_file、run_tests、git_diff、finish;createRuntime 注册真实工具、policy和 budgets。workspace copy 初始化本地 Git baseline,autoApproveWrites 仅在测试配置明确开启。最终 Node test pass,Git diff出现减法到加法,源 example 仍保留 bug。

Fake action 的 callId 唯一且结果回填 messages,验证 Agent Loop 而不只是一个 for loop。steps 六、toolCalls 五与协议对应;finish 不计工具。预算设置足够但有限,重复动作检测有效。测试无需 OpenAI key,任何网络调用都属于回归。

Provider E2E 与 Runtime E2E 分开更容易定位:第 37 课契约 fixture验证 HTTP映射,本课 Fake Provider 验证 Agent组合。少量 staging smoke再连接真实 Provider,但不作为普通 CI唯一证据。把所有层放一个真实云测试,失败时无法判断网络、模型还是工具。

负面 E2E 至少包括权限未批准写入、path traversal、search zero、edit duplicate、test fail、empty diff、budget exhaust、cancel、Provider invalid action和 cleanup fail。每条验证无越界副作用、status准确、源不变。安全属性常在失败路径,happy path 不能代替。

端到端测试要控制运行时间和输出。小 fixture、不安装网络依赖,命令使用当前 Node,临时目录 afterEach 兜底;并行运行每个独立 root/Git config。flaky E2E 不能简单重试取绿,先隔离根因,否则发布门禁失去信任。

变更范围是验收的一部分。任务只允许 src/add.ts,git_diff allowedPaths 发现 package/test 变化应失败或高风险;changedFiles 精确一项。模型即使修复功能但顺手格式化全仓,也不算最小安全修复。reviewer 解释为什么 Diff 必要且充分。

测试充分性不能只包含字符串 a + b,因为注释也能骗过。Lab 是流程最小验证,项目用可执行 test;正式 benchmark 加 mutation:把修复重新变错应使测试失败。内容、行为和 mutation 三层 grader降低 false positive。

Trace Viewer 在 Demo 中按六步展示:搜索证据、读取行、唯一编辑 transaction、测试退出码、Diff patch、finish gate。观众可以点击每项而非看终端截图。sourceHash与 eval report link证明运行条件,错误 Trace展示错误 edit 如何被 test阻止。

评测 Runner 对多个 bugfix fixture 每次 fresh copy,聚合成功、成本、steps和 failureKind。本课单场景是 release smoke,不代表泛化能力;下一课比较基线与新版本的分类指标,防一个 Demo成功掩盖其他任务退化。

完整验收运行 good script 得 events严格六步、changedFiles只有 math.ts、sourceHash六十四位;bad order执行前拒绝;乘法 edit在 verification停止;运行前后源 Buffer相同;临时 copy被删除。总项目 E2E再证明真实测试与 Git diff。

端到端场景应有明确 Given/When/Then。Given 是 source fixture hash、失败基线、任务 prompt、Provider script、权限与预算;When 是 Runtime 在独立 workspace 运行;Then 是状态、步骤、工具数、测试、Diff、源原态和资源清理。把前置条件写完整,失败时才知道哪项环境漂移。

基线失败要保存证据。运行 Agent 前在副本执行目标测试,预期因 add 返回减法失败;若通过,fixture 已修复或测试失效,E2E 应停止而不是继续 edit。某些 feature 场景基线并非失败,则 task manifest 明确 expectedBaseline,不能套用统一红灯假设。

任务 prompt 只说行为目标,不泄露 a - b 或替换答案;Fake script知道 fixture细节是模型替身,但测试仍应让 search 找到证据。否则 Agent 成功可能来自答案硬编码,不代表检索/读取组合有效。黑盒断言不查看 Provider内部 script,只观察工具 Trace。

AGENTS.md 与选中 Skill 也应进入真实 E2E:系统上下文要求先测后改、限制路径,bug-fix Skill 提供工作流;Provider request fixture 验证这些上下文存在但秘密不存在。若 Instruction/Skill 加载失败,任务不应悄悄走无规则模式,发布 smoke明确报告。

权限流程有两条 E2E。未批准 write 时 edit_file 返回 PERMISSION_REQUIRED,文件与后续测试不变,CLI 稳定退出;测试配置显式 autoApprove 或注入一次批准后,edit 才执行。不要在测试中全局禁用 Policy,否则成功路径绕开了产品最关键边界。

预算 E2E 设 maxSteps 五而脚本需要六,预期 budget_exceeded、无 finish;已发生 edit 是否保留取决于事务/产品政策,结果明确列 changedFiles与恢复建议。正常路径预算足够但有限,usage总和与步骤一致。这样证明门禁不是无限资源下才成立。

取消可在 search、edit前或 test运行中注入。预取消零工具,运行中取消向 TestRunner子进程传播并等待退出,Runtime不finish,temporary仍清理。取消后源原态和无孤儿 PID比 status字符串更重要。Trace最后事件标 cancelled与安全 reason。

Checkpoint E2E 在 edit后崩溃,保存 state、workspace hash、已执行副作用;恢复不能重复 edit,必须重新 test/diff,最终完成。若 workspace被人工改变或指令/Skill hash不同,恢复停止。恢复场景让前几周状态设计在真实流程中接受验证。

测试失败后模型可能继续 read/edit再测,真实状态机允许迭代而不是固定六步。每次 write使 validation epoch增加;test/diff证据绑定 epoch,finish要求最新 epoch均通过。固定 Lab script是单迭代最小例,生产 contract用 epoch表达通用门禁。

重复动作防循环。Provider连续两次同 search/read 或相同失败 edit达到 maxRepeatedActions,Runtime阻止并报告 repeated_action,避免烧预算。E2E fixture可以让 Fake Provider重复一个 action,断言未无限运行、usage有限、源不变。成功流程中的合法重测要有不同 epoch,不误判。

工具 result 回填给模型必须受限。search/read内容截断并带 provenance,test输出脱敏,diff摘要有限;否则 E2E小项目通过,大仓库上下文爆炸。测试可让文件包含假 token,确认 Trace/model message不泄露。端到端是发现跨层脱敏缺口的最佳位置。

Git 初始 commit 使用测试局部 user.name/email与 hooks禁用,不依赖开发者全局配置。line ending与 autocrlf固定,diff路径稳定。Windows/macOS/Linux runner都应得到同样 changedFiles与语义 patch;显示hash若包含mode/line ending,fixture生成规则需规范。

依赖安装不应发生在每次 E2E公网。fixture使用Node内置测试或 workspace预装锁依赖,网络关闭;需要包管理器时只用离线缓存与 lockfile。测试时间有上限,慢安装与 Agent能力分开测。否则 Registry波动会被误归因 Runtime。

source原态验证覆盖错误路径。invalid order虽然可在copy前拒绝,也要确认未创建残留;错误 edit/test fail后 finally计算source hash并清理。若源hash改变,污染错误优先于原测试失败并阻止整个suite后续,以免污染扩散。

结果完整性可计算 run evidence hash,规范包含fixtureHash、taskHash、action type序列、changed path/hash、test status、diffHash与final status,排除时间/temp path。Eval报告引用它,Demo链接Trace;任何artifact被改动可检测。hash不代替签名,发布证据可对根hash签名。

端到端安全扫描在运行后搜索唯一 secrets、绝对HOME、临时路径和ANSI控制字符,覆盖stdout/stderr、Trace、checkpoint、Diff、Eval。单模块脱敏测试可能都绿,但某个组合错误仍拼入原始Error;全产物扫描是最后防线。

性能基线记录小fixture总耗时、每步延迟、最大内存与输出字节。主要目标不是追求毫秒,而是发现新Trace/Context/Plugin让启动翻倍或资源无界。阈值留合理平台余量,性能退化与功能回归同样进入第39课比较。

真实Provider smoke使用无敏感临时fixture、最低权限和费用上限,模型行为不确定所以只验证能走安全流程或正确失败,不要求每次同patch。确定性Fake E2E仍是发布门禁;真实smoke提供生态信号,失败后人工分析而不反复重试烧费。

人工探索测试让开发者故意要求错误路径、让测试命令失败、取消中途和拒绝write,观察CLI行动建议与Viewer证据是否可理解。自动断言保证不变量,人类走查发现信息架构问题。课程博客的目标是读者能独立复现这条调查链。

已知限制要诚实列出:单fixture不代表大型仓库,Fake决策不测模型规划,字符串Lab verification弱于真实测试,source hash只证明最终未变。用Evaluator、多类别任务、staging Provider和安全审计逐层补足,而不是把一次绿色E2E称为生产就绪。

发布门禁调用这条E2E时固定commit、依赖、Node与fixture hash,失败上传受控evidence,成功记录hash。合并后部署前再跑一次,确认构建产物与分支相同。线上无法直接修改真实用户仓库做smoke,使用沙箱demo workspace验证CLI/API。

最终承诺是“模型不能靠自述赢得completed”。只有工具事实形成search/read/edit因果链,独立test和真实Diff绑定最新状态,workspace边界与source原态成立,Runtime才转完成。这个原则是整个Coding Agent课程从脚本走向工程系统的核心。

评审一次E2E实现时,可以要求作者现场破坏三件事:把动作顺序交换、把正确加法改成乘法、让write权限保持未批准。合格系统分别在协议、verification和Policy层失败,三次都不污染源;若全都只返回同一个“运行失败”,虽然安全停止,诊断层仍需改进。

E2E报告还应列出每个安全控制实际由哪一层执行:CLI负责配置和退出,Agent Loop负责预算/完成状态,Tool负责schema,Sandbox负责命令与权限,Editor负责事务,Trace负责证据,Evaluator负责评分。黑盒测试覆盖整体,模块测试证明具体责任,避免所有希望都压在一个巨型集成用例上。

完成后的patch不应自动应用到用户真实仓库。基准副本用于证明能力,产品运行在真实workspace时仍展示Diff、测试与风险,等待既有授权策略决定是否保留;发布/提交更是独立外部动作。E2E绿色只授权“这个场景完成”,不扩张用户操作范围。

当真实任务需要多文件修改,changedFiles从一项扩展为允许集合,test证据覆盖组合状态,diff reviewer检查跨文件接口;原则不变。先在多个小fixture验证新增/删除/rename,再把大型仓库纳入benchmark,保持每个失败可定位和source可恢复。

无论任务规模如何扩张,最新代码、最新测试与最新Diff必须属于同一验证epoch,完成状态才有一致含义。

证据必须同步更新。

始终如此。

运行与验证

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 38
pnpm --filter @coding-agent/agent-core test

成功证据为六个有序 event、changedFiles=src/math.ts、64 位 sourceHash;运行后原 math.ts Buffer 与运行前完全相等。

真实运行输出

text
✓ tests/lab.test.ts (5 tests)
Test Files  1 passed (1)
Tests       5 passed (5)

总项目 E2E 还应看到 Node 测试 pass 1、Git Diff 的 a - ba + b 和 result steps=6/toolCalls=5。源 example 内容仍是错误版本,证明评测隔离。

常见失败与排查

故障案例 1

症状:最终代码看似正确,但 Agent 未 search/read,或 test 之前就 finish,审计无法解释来源。

根因:E2E 只断言最终字符串,没有状态机和有序 event。

定位:交换动作、删除步骤、finish 后追加 edit,检查 Runtime是否仍 completed。

修复:用协议/状态机约束合法转移,写后重置验证;结果保存顺序证据并逐项断言。

故障案例 2

症状:把减法改乘法也完成,或模型声称 tests passed/changedFiles 不为空就通过。

根因:test/diff 取模型声明,verification 与 workspace 外部事实没有连接。

定位:注入错误 edit、伪造 summary/changedFiles,独立读取文件、运行命令和计算 baseline diff。

修复:受控 Runner 执行独立测试,changedFiles由真实树/Git计算;两项成功后 Runtime才接受 finish。

故障案例 3

症状:第一轮后第二轮无需修复就通过,源 fixture 或 task.json 已改变,tmp不断残留。

根因:直接在 source运行,或 finally/原态 hash 只在成功路径。

定位:连续运行两次、比较源全树 before/after hash,成功失败后检查临时路径。

修复:每轮唯一 copy,source只读加前后规范 hash;finally导出安全证据并始终清理。

课后作业

把脚本替换为 Fake Provider 驱动的 Agent Loop,接入真实 search/editor/sandbox/trace 包;保留同一 fixture 与黑盒断言,比较两种实现证据。

验收 Rubric

维度通过标准常见扣分
流程六步严格且有序跳步或乱序
门禁test+diff 后才能 finish自报完成
隔离临时副本、源哈希不变修改基准源
证据events+changedFiles+hash只返回 completed

总项目增量

总项目包:@coding-agent/agent-core

总项目路径:packages/agent-core/src/bug-fix-runtime.tsfixtures/bug-fix

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

端到端能力已闭环;下一课把新版本与基线做分类回归判断。

延伸阅读

  • Scenario test 与 contract fixture。
  • Hermetic end-to-end testing。
  • Verification gate 和 false completion。

方案对比与工程取舍

动作脚本 Runtime 快且故障精确,适合教学状态机;真实 Agent Loop覆盖更多集成但调试更复杂。两者不是替代:Lab先锁定最小协议,总项目 Fake Provider走真实工具,staging再少量真实Provider。分层测试让每层失败有归属。

内容 includes grader 简单确定,却可能被注释欺骗;真实测试更可信但受环境和覆盖影响;mutation进一步检验测试能抓回归。课程由简到繁组合三层证据,最终完成门禁应以行为测试和 Diff为主,不依赖单字符串。

在共享 workspace执行最接近用户现场,却可能污染状态和并发冲突;副本/工作树提供复现与回收,最终 patch再由用户批准应用真实仓库。Benchmark 必须隔离,交互产品可在真实 workspace但使用事务/checkpoint;场景不同,边界要明确。

下一课衔接

单个 Bug Fix 已有可复核完成证据,但一个成功 Demo 不能证明版本整体更好。下一课 Eval 加固会比较总体、成本、步骤与每个 category,用显式阈值和 regress 优先级阻止“总分不变但 bugfix 退化”或“质量小升、成本翻倍”的发布。

从零实现 Mini Code Agent Runtime