Chainlink 修复 Flaky 测试的 AI Skill:fetch-flaky-tickets 的 JQL 检索循环与工单筛选机制详解
Chainlink 修复 Flaky 测试的 AI Skill:fetch-flaky-tickets 的 JQL 检索循环与工单筛选机制详解
本文围绕 Chainlink 仓库中 tools/test 目录下 fix-flaky-tests AI Skill 的参考文档 fetch-flaky-tickets.md 展开。读完后,你将理解该 Skill 如何通过一条 JQL 检索循环批量拉取指定 JIRA 项目中“eligible(合格)”的 flaky-test 工单:如何用零成本检查排除跨仓库工单、如何用 LSP/语义搜索/grep 三级降级策略把工单解析到本地真实存在的测试函数、如何以 nextPageToken 分页循环直到凑满 N 条,以及最终产出的 slim record(精简记录)JSON 结构如何避免反复调用 Atlassian MCP,并无缝衔接后续的 claim-ticket.md 领取流程。
1. 定位:它是 fix-flaky-tests Skill 的“工单供给层”
Chainlink 的 tools/test/AGENTS.md 描述了该目录的三大目标:用单一命令跑测试、自动重跑并分析结果以定位 flake/慢测试,以及提供一个 AI Skill fix-flaky-tests——一个“能分析 GitHub Actions 日志并管理 JIRA 工单生命周期”的诊断式测试修复技能。Skill 的主入口是 SKILL.md,而 JIRA 相关操作则被拆分为一组“可包含的参考文件(includable references)”,其索引见 jira.md。
按 jira.md 定义的决策逻辑:
- 用户直接给出具体 JIRA 工单 → 走
claim-ticket流程; - 用户要求处理 N 张合格工单(“work on N eligible tickets”) → 走
fetch-flaky-tickets循环; - 否则用
find-flaky-test-ticket按测试名反向查找关联工单。
本文主角 fetch-flaky-tickets 正是第二条路径的核心:它负责“从 JIRA 项目里捞出 N 张真正可以在当前仓库里动手修的 flaky-test 工单”。这一点很关键——JIRA 里 flaky-test 标签的 Open 工单往往横跨多个仓库、多个项目,若不加筛选,Agent 会浪费时间在不属于自己的代码上。
2. 输入要求:三要素缺一不可
文档 <requirements> 一节明确了该流程的三个前置输入:
| 输入 | 来源 | 说明 |
|---|---|---|
| JIRA 项目 Key | 用户提供 | 作为 JQL 中 project = {KEY} 的取值 |
| 工单数量 N | 用户提供 | 目标合格工单数,也是 maxResults 的取值 |
| 当前仓库标识 | git remote get-url origin 提取 {owner}/{repo} |
用于跨仓库工单的零成本比对 |
其中“当前仓库”的提取方式值得注意:不是读 go.mod 或目录名,而是 git remote get-url origin 返回的 remote URL 中的 owner/repo 段。这样做的好处是与 JIRA 工单里 customfield_13009 字段(形如 github.com/{owner}/{repo}/path 的包路径)采用同一坐标系做比对——后者取 “github.com/ 之后第 2、3 段” 即为 owner/repo,与 remote URL 的提取方式对称,比对逻辑因此可以做到纯字符串、零 API 调用。
前置依赖上,jira.md 要求 Atlassian MCP 可用且已认证,否则 STOP 并提示用户安装/认证;所有操作还需要两个身份信息:cloudId(来自 mcp__atlassian__getAccessibleAtlassianResources)与当前用户的 accountId(来自 mcp__atlassian__atlassianUserInfo)。
3. JQL 检索循环:分页拉取 + 两级过滤
3.1 检索参数
文档给出的循环骨架(伪代码)如下:
results = []
cursor = null
while len(results) < N:
fetch N issues via mcp__atlassian__searchJiraIssuesUsingJql:
jql: project = {KEY} AND labels = "flaky-test" AND status = "Open" ORDER BY created DESC
fields: ["summary", "description", "comment", "status", "assignee",
"customfield_13009", "customfield_13007"]
maxResults: N
nextPageToken: cursor (omit on first call)
for each issue (in order):
1. Repo check ... 2. Test function check ... 3. Eligible → append
cursor = nextPageToken from response
if no more pages: break
几个要点:
- JQL 固定为
project = {KEY} AND labels = "flaky-test" AND status = "Open" ORDER BY created DESC。即只检索带flaky-test标签、状态为 Open 的工单,按创建时间倒序(新单优先)。 - fields 是显式白名单:
summary、description、comment、status、assignee,外加两个自定义字段customfield_13009与customfield_13007。这与 jira.md 的绝对约束一致——“Never return raw JIRA API objects,caller only receives slim records”:Agent 只持有精简字段集,避免把庞大的原始 JIRA 对象灌入上下文。 - 分页契约:首次调用省略
nextPageToken;之后把响应中的nextPageToken作为cursor传入下一轮,直到凑满 N 条或没有更多页。注意每轮maxResults恒为 N,即“一轮最多拉 N 条”,过滤后不够就继续翻页——这是“fetch N issues”与“results < N”两个 N 的微妙区别。
3.2 第一级过滤:Repo check(零成本)
对每条按序返回的工单,先做仓库归属检查:
- 从
customfield_13009(package 字段)提取{owner}/{repo}——即github.com/之后第 2、3 段——与git remote get-url origin得到的当前仓库比对; - 不匹配 → 跳过,并累加
cross_repo++计数器; - 若
customfield_13009缺失,则回退到description中扫描https://github.com/{owner}/{repo}链接或Repo:/Repository:字段。
这一步被称为 “zero-cost”(零成本),因为它不需要任何额外的 API 调用或本地代码检索,纯字符串比对即可完成。把最廉价的过滤放在最前面,是典型的漏斗式筛选设计:大量跨仓库工单在这里被快速剔除。
3.3 第二级过滤:Test function check(三级降级定位)
仓库匹配后,需要确认工单指向的测试函数在当前代码库里真实存在:
- 从
customfield_13007(test_name 字段)提取顶层函数名——取第一个/之前的部分(/之后是子测试名,见第 4 节的 slim record 说明);字段缺失时,回退为标题中最长的TestXxxtoken; - 定位该函数的实现,按可用性三级降级:
LSP可用 → 用 LSP 做 definition lookup(SKILL.md 甚至要求先ToolSearch select:LSP加载 LSP 工具 schema,并用项目内一个 go 文件验证 LSP 可用);Code Review Graph可用 → 调用mcp__code-review-graph__semantic_search_nodes_tool做语义搜索;- 最后手段 →
grep -rl "func {TestName} ."。
- 找不到 → 跳过,累加
not_found++。
这个设计保证了“eligible”的语义非常严格:不仅工单属于本仓库,而且其测试函数能在本地源码中定位到。否则后续的诊断循环(make test ARGS="diagnose ...")将无从下手。cross_repo 与 not_found 两个计数器为调用方提供了漏斗可观测性——能直接回答“为什么凑不够 N 张”。
3.4 合格工单 → slim record
通过两级检查的工单即“eligible”:构建 slim record(结构见第 4 节)追加进 results,一旦 len(results) == N 立即停止。循环结束后,文档指示:通过 claim-ticket 领取这些合格工单,且可跳过其中的 pre-requisites(前置检查)——因为 fetch 阶段已经完成了状态/仓库/测试函数三项校验,无需重复。这是一个明确的流程编排优化:claim-ticket 的 pre-requisites 与本节的两级检查是同构的(状态 Open 由 JQL 保证、repo check 与 test function check 完全一致),复用结果避免了重复工作。
4. 产出物:slim record 的精简 JSON 结构
所有 JIRA 数据在 Agent 内部流转时,必须使用 slim-record.md 定义的规范结构。jira.md 明确写道:“You MUST use this JSON structure to pass data around in order to avoid calling Atlassian MCP multiple times to read information”——一次抓取、全程复用,避免下游每个子流程都重新调 MCP 读单。
{
"jira_key": "KEY-NNN",
"title": "string",
"description": "string",
"package": "github.com/owner/repo/path | null",
"test_name": "TestFoo | TestFoo/subtest_name",
"previous_attempts": [
{
"outcome": "MISMATCH | ABANDONED | FIXED",
"date": "YYYY-MM-DD",
"summary": "string",
"excluded_approaches": ["string"],
"rejection_reasons": ["string"],
"recommended_next_step": "string | null",
"full_text": "string"
}
],
"original_assignee": "string | null",
"skip_reason": "string | null"
}
字段规则(field-rules):
package:即customfield_13009,缺失时为 null;test_name:即customfield_13007,含子测试的完整路径(如TestFoo/subtest_name);缺失时取标题中最长的TestXxx/testXxxtoken。这也解释了第 3.3 节为何要“取第一个/之前的部分”作为顶层函数名;previous_attempts:按investigation-comment的解析规则从工单评论中解析出的历次调查尝试(outcome 只能是 MISMATCH / ABANDONED / FIXED 三态)。这个字段把“工单历史”压缩为结构化经验,防止 Agent 重复踩已被否定的修复方案;original_assignee/skip_reason:由子代理在 claim 阶段填写(前者记录领取前的原 assignee,供 abandon/mismatch 时恢复;后者记录跳过原因)。
5. 衔接下游:从“eligible 工单”到“真实修复”
fetch 阶段只是流水线的第一环。结合 SKILL.md 可以看到完整闭环:
- 领取:
claim-ticket将工单指派给当前用户(mcp__atlassian__editJiraIssue设置assignee.accountId)并流转到In Progress; - 诊断:使用
tools/test的 Go 测试 harness,从仓库根目录执行make test ARGS="diagnose [harness_flags] -- [go_test_flags] ./path", 其中 slim record 的test_name直接映射为--run '^TestName$',package映射为./path。tools/test/README.md 说明了该 harness 基于 testrig,负责go test无法独立完成的“一次性公共初始化”(如创建 Postgres 数据库——Chainlink 的测试共享单一 Postgres,diagnose 循环会新建 DB,见 SKILL.md 的<tests-context>),并提供--iterations N、--fail-fast-on=(timeout|slow)、--parallel-iterations N等标志; - 验证与结论:SKILL.md 给出的迭代配置表直接支撑了“验证 flake 存在/消失”的判定标准——5 次迭代漏检率约 50%,30 次约 10%,150–500 次(Deep profile)降到 2% 以下,代码变更后至少需要 Standard(30 次)级别的复跑才可宣告 FIXED;
- 收尾:无论结果如何,每张工单都要写 investigation comment 并流转——abandoned/mismatch 回到
Open(恢复原 assignee),fixed 进入In Review(且按 jira.md 的 assignee 规则,修复者保持 assignee 身份,FIXED 时不得取消指派)。
6. 设计要点回顾
从这份参考文档可以提炼出几个可迁移的工程实践:
- 漏斗式过滤,成本递增:JQL 在数据库侧先筛掉非 flaky-test/非 Open 的工单;repo check 零 API 成本筛掉跨仓库工单;test function check 才动用 LSP/语义搜索/grep 做本地代码检索。越贵的检查越靠后。
- slim record 作为数据契约:所有 JIRA 信息以一份精简 JSON 在子代理间传递(jira.md 强调 “caller only receives slim records”),既控制了 Agent 上下文体积,又保证后续 claim/transition/abandon 等步骤不重复调用 MCP。
- 严格的 eligibility 语义:一张工单“合格”= 本项目 + Open + 本仓库 + 测试函数可定位。宁可少取、继续翻页,也不把不合格的工单交给修复循环。
- 计数器留痕:
cross_repo++、not_found++让调用方能解释“为何结果少于预期”,而不是静默失败。
7. 适用前提与限制
- 该流程面向 Atlassian JIRA(Cloud)+ Atlassian MCP 环境,且工单需遵循统一字段约定:
flaky-test标签、customfield_13009(package)与customfield_13007(test_name);字段缺失时依赖 description 文本或标题 token 的回退解析,精度相应下降。 - SKILL.md 明确约束:诊断运行仅允许在
core/、deployment/等包上自动执行,且禁止裸用go test,只允许make test ARGS="diagnose ..."(单次运行用--iterations 1);LSP 不可用时按 code-review-graph → grep 的顺序降级。 - 文档本身是 Agent Skill 的“参考剧本”,不是给人执行的 shell 脚本——其中的 MCP 工具名(如
mcp__atlassian__searchJiraIssuesUsingJql)只有在具备对应 MCP 服务器的 Agent 环境中才成立。
总结来说,fetch-flaky-tickets.md 虽然篇幅很短,但它定义了 Chainlink flaky-test 自动化流水线中“工单供给”环节的完整契约:一条固定 JQL、两个字段驱动的两级零成本/低成本过滤、一个规范 slim record,以及向 claim-ticket.md 的免检交接。配合 tools/test/README.md 的 harness 用法与 SKILL.md 的诊断循环,构成了“从 JIRA 工单到修复验证”的完整闭环。