
最近折腾了一个从前端触发测试修复的 Agentic 工作流,踩了不少坑,这篇把整个过程说清楚。
我们前端每次改个 UI 组件,就引发一场小噩梦:测试挂了,测试自动化(TA)团队被叫过来,然后就是没完没了的来回拉锯。
我琢磨着能不能通过在工作流中引入一个无头 Agent来减少这种摩擦——让 Agent 自动接手失败的测试并尝试修复。
不过搞这种自动化确实是个硬骨头——要设计有意义的工作流、处理可能变成死胡同的跨依赖关系、还有一路上各种未知问题。这篇文章里,我会带你走过问题分析、设计决策和权衡,以及端到端的实现过程。
在前端这边,我们配置了一些 CI 步骤,其中一项就是运行测试自动化。但这些测试经常因为前端改了 HTML 结构或者用户流程而挂掉。一旦发生,就得走下面流程图里的步骤。

简单总结:
这个工作流在每个前端 feature 分支都会触发。步骤必须按顺序执行,整个流程涉及至少四个工程师(写代码的和 review 的)、上下文切换,还有跨团队依赖。而且中间有一段测试覆盖率会丢失:测试通过了,只是因为被跳过了。
做这个实验时,我们决定先聚焦在一个具体且容易修复的场景:前端改了 UI 组件,需要选择器变更的情况;只用冒烟测试作为卡口。下面给你更多背景信息。
在前端应用中,我们实现用户交互的组件。这些组件可以通过文本、输入框 placeholder、role 或者屏幕阅读器的 aria-* 属性来访问,最后才会 fallback 到 data-testid。这些标识符——选择器(或者某些库叫的 locator)——被 Testing Library、Playwright 或 Cypress 这类测试库用来执行自动化测试,模拟用户操作。
只要用来标识测试组件的文本、label 或其他内容被改了,测试就会挂。你可能觉得这是小概率事件,但实际上在以下场景经常发生:
冒烟测试是一小套端到端测试,只覆盖应用最关键的流程。核心思想不是测所有东西,而是快速回答一个问题:应用还能正常工作吗?因为数量更少、执行更快,它们比完整测试套件成本更低,适合在每个 feature 分支运行——对于这个实验来说,也意味着 Agent 要处理的失败更少(且更稳定)。如果想深入了解,这篇文章是个不错的起点。
这个案例不是放之四海而皆准的方案,但能给你一些启发。我们有这些特定配置:

整体思路是:当前端有变更时,CI 管道运行冒烟测试,如果失败,就通过 GitHub REST API 向 TA 仓库发送 repository_dispatch 事件来触发 TA 的工作流 Agent,Agent 然后尝试修复未通过的测试。如果成功,会创建一个 PR 供人工检查。如果这个 PR 被合并,前端 CI 管道就能通过。
几点需要考虑的:
经过大量来回讨论确定了高层设计之后,我开始琢磨细节。
要实现这个,需要回答几个问题:
考虑到前端的每个 feature 分支都会触发 CI 管道,加上成本效率和可控性(尤其是测试这个自动化期间),我选择在特定条件下触发 Agent。需要 feature 分支部署到特定环境(我们有超过 4 个环境,可以同时测试多个 feature 分支)。我们称这个环境为QA 环境。
有这个环境也是必须的,因为约束条件中 TA 只能针对已部署的环境运行冒烟测试。
自动化设计有多个步骤,其中之一就是决定在哪儿触发 Agentic 流程。如约束条件中所述,我们同时用了 Drone 和 GA。
我们的 Drone 已经够拥挤了——它在所有分支包括 main 上运行进程。我意识到用 GA 可以更好地控制触发时机和方式——它们的界面对测试和手动触发更友好。而且我可以为 Agent 创建独立的流程,不和其他进程混在一起。
基于这些原因,我决定用 GA 来运行 Agentic 工作流,因此连接点应该是GA 触发 Agent。
这个自动化的发起者是前端,需要满足两个条件:冒烟测试失败 且 已部署到 QA 环境。由于前端 CI 跑在 Drone、前端 CD 跑在 GA,这两个条件在不同地方检查。我有两个选项:
第一个选项成本更高——需要把冒烟测试步骤引入 GA,会在 CI 和 CD 管道中重复。
第二个选项只是一个简单检查。因为我们用 GitHub labels 来追踪部署到哪个环境。已经有一个流程:给 PR 添加 label 就会触发部署到那个环境。
因此,我们可以在冒烟测试步骤之后检查 GitHub label。如果冒烟测试失败且部署 label 设置为 QA 环境,那么Drone 发送 dispatch 事件到 TA。
由于 CI 和 CD 在不同环境执行,这里有一个权衡:Drone 可能收到并识别到 label,但 GA 的部署流程可能失败,或者 Drone 检查时分支实际上还没部署完成。我接受这些权衡,因为最坏情况不过是测试跑在没有前端变更的环境上,导致误报失败。
关于竞态条件,label 一旦添加,通常 CD 在 5 分钟内完成。整个 CI 管道通常需要 15 分钟,检查作为 CI 管道最后一步,时间上足够缓冲。
前端创建了一个破坏冒烟测试的新功能,已部署到 QA 环境,dispatch 事件已发送。接下来呢?Agent 应该做什么?
整个目标是修复被破坏的冒烟测试,方法是通过更新选择器。为此我做了一些决定:
三个主要结果,加上两个边缘情况:
虽然可以在 GA 工作流页面看到所有日志,但我想尽可能透明,在前端 PR 上给出整个流程结果的消息。
然后,大部分定义都设定好了,接下来可以看看 Agent 具体怎么做的,以及围绕它的基础设施。
如你所见,实现这个流程有多个步骤。
工具链(harness)是围绕模型的一切,塑造 Agent 如何工作——指令文件、技能、命令、脚本和护栏——让它能可预测地运行,不需要人一步步引导。
代码库的初始状态完全没有为 AI 做准备。为了不扩大项目的影响范围,我决定只创建足够实现这个 POC 的工具链。创建了两个资产:
CLAUDE.md 指向它,使 Claude 和 OpenAI 模型都能读取。常规的指令文件,注入到每个 Agent 会话中(不深入讲这个,网上有很多文档)。getByRole,然后是 getByLabel,以此类推。用这些,我在自己的 Agent(本地 Claude Code CLI)上测试了它的表现。
也创建了其他命令,但它们属于自动化本身,所以会在后续章节详述。
另一个问题是测试无法在 QA 环境运行。数据耦合到特定环境,选择器也是。这和 AI 或自动化无关,但会影响开发能力。因此,我做了额外的准备工作:创建函数为每次测试运行 setup/create 和 teardown/delete 数据,以及让选择器环境无关化。
整个流程的可视化图表如下:

流程从前端开始,我自然先从那里开始,但工作量不大:我在 drone.yml 的冒烟测试步骤之后加了一个新步骤:
- name: Smoke test commands:
# Smoke tests instructions - name: Dispatch selector repair environment: GITHUB_APP_TOKEN: from_secret: GITHUB_APP_TOKEN REPAIR_ENVIRONMENTS: QA_Env commands: - sh ci/drone/dispatch-selector-repair.sh depends_on: - Smoke test when: status: - failure
(注:这是示例代码片段,不能直接使用。)
作为变量,需要 GitHub App token 来启用跨仓库通信,REPAIR_ENVIRONMENTS 是启用触发此自动化的环境镜像列表。目前只有 QA 环境启用。
我可以在 YAML 文件中添加所有指令,但提取到 ci/drone/dispatch-selector-repair.sh 中以分离关注点并提高可读性。在这个脚本中做了以下事情:
frontend-smoke-failed 事件然后,TA 这边接收这个事件和 payload。
为了接收事件,创建了一个 GA 工作流。当收到事件时触发 selector-repair.yml:
repository_dispatch: types: [frontend-smoke-failed]
也可以手动触发,虽然主要是为了测试目的,在评估 Agent 性能时用到。
这个工作流:
codex exec 命令,运行修复选择器命令(如下所述,正式名称 repair-selectors-ci)如你所见,LLM 只涉及一个特定步骤。之前和之后都是确定性脚本,既降低成本也提高质量。Agent 只在能实际在启用环境中运行测试时才会触发;Agent 完成后,脚本会整理 PR 评论的消息体返回给前端。
除了设置章节的共享工具链,CI 运行还添加了两个命令。即:
修复选择器命令是核心 Agent 逻辑所在。总结一下,它做了:
1. 读取环境变量输入:要测试的环境、触发运行的前端 PR,以及失败 specs 列表。
2. 管理持久分支:每个前端 PR 一个分支,在多次运行中复用和追加,而不是每次重建,因为同一分支的每个 commit 都会触发新运行。它永远不会 rebase 或 force-push,因为 commit 历史作为审计日志。另外,如果前端侧有新 commit,GA 会通过 concurrency: cancel-in-progress 取消当前运行,不会继续处理过时代码。如果 commit 已经推送了,就留在那里。
3. 确定测试什么:要么是特定的失败 specs,要么是手动运行整个冒烟套件。如果之前的修复运行已经在持久分支上提交了修复,就被归类为"已修复"。
4. 将每个失败分类到三个桶之一:
5. 修复 rot 分类的选择器,通过探测实时页面的可访问信号(role、label、text、placeholder 等),选择精确解析到唯一元素的最佳选择器,并验证修复实际让测试变绿后才保留。
6. 运行质量门 — lint/prettier 检查,清理任何临时/探测文件 — 然后才允许提交。
7. 打开或更新 draft PR(从不合并)—— 这是打开修复 PR 命令。
8. 写入结构化 JSON 摘要(repair-summary.json)作为返回调用工作流的唯一通信渠道 —— 列出什么被修复了、什么作为回归升级了、什么无法复现、之前的运行已修复了什么,以及任何环境错误 —— 有严格规则规定哪些场景哪些字段必须为空,确保面向人的报告永远不会误导。
之后,payload 被发送回 selector-repair.yml,由"评论反馈到前端 PR"最后一步处理。这是确定性代码,执行我们之前在"输出结果:Agent 应该报告什么?"章节讨论过的反馈给前端仓库。
在这个实验之前,一个破坏性 UI 变更意味着跳过测试、在覆盖缺失的情况下合并,至少四个工程师跨两个团队协调。使用这个流程后,前端直接在 PR 中获得反馈,TA 审查一个 draft 修复而不是从头写,当 Agent 成功时,测试不再需要在 main 上挂着跳过。
对我来说最大的教训是:Agent 是工作中最小的部分。大部分精力都花在周围的东西上:决定它什么时候应该运行、连接两个仓库和两个 CI 工具、使测试能在共享环境运行,以及写确定性检查让 LLM 只在真正能帮上忙时才调用。
还有改进空间,我相信这可以作为超越本地开发使用 AI 的自主系统的案例研究。未来是 Agent 在云端工作,不需要人驱动每一步——为此,我们需要扎实的工具链和护栏,持续观察输出以提高质量、降低成本并加快速度。