Chainlink 修复 Flaky 测试的 AI Skill:fetch-flaky-tickets 的 JQL 检索循环与工单筛选机制详解

原创2026-09-15 10:11:061,377 阅读
文章标签:区块链Web3后端

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

几个要点:

  1. JQL 固定为 project = {KEY} AND labels = "flaky-test" AND status = "Open" ORDER BY created DESC。即只检索带 flaky-test 标签、状态为 Open 的工单,按创建时间倒序(新单优先)。
  2. 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 对象灌入上下文。
  3. 分页契约:首次调用省略 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(三级降级定位)

仓库匹配后,需要确认工单指向的测试函数在当前代码库里真实存在:

  1. 从 customfield_13007(test_name 字段)提取顶层函数名——取第一个 / 之前的部分(/ 之后是子测试名,见第 4 节的 slim record 说明);字段缺失时,回退为标题中最长的 TestXxx token;
  2. 定位该函数的实现,按可用性三级降级:
    • 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} ."。
  3. 找不到 → 跳过,累加 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/testXxx token。这也解释了第 3.3 节为何要“取第一个 / 之前的部分”作为顶层函数名;
  • previous_attempts:按 investigation-comment 的解析规则从工单评论中解析出的历次调查尝试(outcome 只能是 MISMATCH / ABANDONED / FIXED 三态)。这个字段把“工单历史”压缩为结构化经验,防止 Agent 重复踩已被否定的修复方案;
  • original_assignee / skip_reason:由子代理在 claim 阶段填写(前者记录领取前的原 assignee,供 abandon/mismatch 时恢复;后者记录跳过原因)。

5. 衔接下游:从“eligible 工单”到“真实修复”

fetch 阶段只是流水线的第一环。结合 SKILL.md 可以看到完整闭环:

  1. 领取:claim-ticket 将工单指派给当前用户(mcp__atlassian__editJiraIssue 设置 assignee.accountId)并流转到 In Progress;
  2. 诊断:使用 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 等标志;
  3. 验证与结论:SKILL.md 给出的迭代配置表直接支撑了“验证 flake 存在/消失”的判定标准——5 次迭代漏检率约 50%,30 次约 10%,150–500 次(Deep profile)降到 2% 以下,代码变更后至少需要 Standard(30 次)级别的复跑才可宣告 FIXED;
  4. 收尾:无论结果如何,每张工单都要写 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 工单到修复验证”的完整闭环。

登录后查看全文
chainlink