Skip to content

fix(ci): make autofix finding replies idempotent - #9463

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/autofix-reply-idempotence
Aug 20, 2026
Merged

fix(ci): make autofix finding replies idempotent#9463
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/autofix-reply-idempotence

Conversation

@wenshao

@wenshao wenshao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the autofix finding-reply step idempotent. The thread fetch now also reads each comment's author and body, and the reply step skips posting when the thread already carries a comment by the autofix bot whose body EQUALS the neutralised body about to be posted. A changed body — new information from a later round — still posts; a threads view without author/body, or a stale/empty one, degrades to the old post-always behavior (best-effort, consistent with the rest of the block).

Why it's needed

The reply path is invoked from every completed address round, and the same finding gets an entry in comment-replies.json again whenever the round crashes and re-runs, a same-run repair regenerates the dispositions, or a later round's agent re-declines an unchanged finding. Each of these re-posts the same reply on the same thread. Measured on 2026-08-16 (full details in #9296): one identical reply landed three times on a merged PR within seconds of each other.

Note on scope: the item in #9296 also mentions using the replies API instead of standalone review submissions — the workflow's reply path already uses POST /repos/{o}/{r}/pulls/{n}/comments/{id}/replies, which emits no pull_request_review event, so this PR only adds the missing idempotence gate.

Reviewer Test Plan

How to verify

This is a CI-workflow change with a pinned test; behavior is also observable in round logs:

  1. Unit level: npm run test:scripts -- qwen-autofix-workflow.test.js runs the pinned test answers the threads it leaves open, which extracts the reply block from the workflow and drives it against a stubbed gh. New scenarios: an identical bot reply already on the thread is skipped (no POST, logged as skipped), a changed body still posts, and the same body last posted by a HUMAN is not treated as a duplicate.
  2. Behavior level: on a PR where a finding was already replied by the bot, a subsequent round that re-declines the same finding with the same wording logs identical bot reply already on the thread and does not double-post.
  3. Degradation: if the thread fetch is stale/empty (the block is best-effort by design), replies post as before — no round stalls on the gate.

Local verification performed: the pinned test passes including the new scenarios; the full pinned suite shows 178/180 green with the 2 failures reproduced on a clean origin/main baseline (bite-check and verification-gate tests, unrelated to this change); the workflow parses as YAML and passes yamllint 1.35.1 (the CI-pinned version) with zero findings; the extracted reply block passes bash -n and shellcheck at error severity; the dedup jq expression was tested against 7 edge-case fixtures (exact bot match → skip; different body → post; human-authored same body → post; legacy nodes without author/body → post; null author → post; orphan comment id → post; empty threads view → post).

Evidence (Before & After)

Before: 2026-08-16 — one identical finding reply posted three times on a merged PR within seconds (#9296). After: the second and later identical posts are skipped at the gate; the first post and any CHANGED follow-up still land.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

N/A — workflow change only; the workflow runs on ubuntu-latest.

Risk & Scope

  • Main risk or tradeoff: the gate compares against the bot login and the exact neutralised body, so a genuinely new round that happens to restate an identical declination is skipped — intended, since the thread already shows it; any new wording posts normally. The thread fetch grows slightly (author + body per comment), bounded by the existing first-100 caps.
  • Not validated / out of scope: the remaining Qwen Autofix: review-event storms and duplicate address dispatch waste runner capacity #9296 P2 items (cron-group semantics, resolve-pr diagnostics) are untouched. Replies posted by flows outside this workflow (e.g. local sessions) are not covered by this gate.
  • Breaking changes / migration notes: none.

Linked Issues

Part of #9296 (second P1 item — finding replies). No closing keyword — the issue still tracks the P2 items.

中文说明

这个 PR 做了什么

让 autofix 的 finding 回复步骤幂等。线程抓取现在同时读取每条评论的作者和正文;回复步骤在 thread 中已存在 autofix bot 发的、与即将发布的 neutralised 正文逐字相等的评论时跳过发布。内容变了(新一轮的新理由)照常发布;线程视图缺少 author/body 或过期/为空时,退化为旧的照发行为(best-effort,与该块其余部分一致)。

为什么需要

回复路径在每个完成的 address 轮次都会执行;当轮次 crash 重跑、same-run repair 重新生成 disposition、或后续轮次的 agent 对未变化的 finding 重新 decline 时,同一个 finding 会再次进入 comment-replies.json,每一种情况都会在同一 thread 上重发相同回复。2026-08-16 实测(完整细节见 #9296):同一条回复在几秒内被发了三遍。

范围说明:#9296 的条目还提到改用 replies API 而非独立 review 提交——工作流的回复路径本来就用 POST /repos/{o}/{r}/pulls/{n}/comments/{id}/replies(不产生 pull_request_review 事件),所以本 PR 只补缺失的幂等门。

Reviewer 测试计划

如何验证

这是带 pin 测试的 CI 工作流变更,行为也体现在轮次日志中:

  1. 单测层:npm run test:scripts -- qwen-autofix-workflow.test.js 运行 pin 测试 answers the threads it leaves open——它从工作流中提取回复块并用 stub 的 gh 驱动。新场景:thread 上已有相同 bot 回复时跳过(无 POST,日志记录 skipped);内容变化时照常发布;人类发的相同内容不算重复。
  2. 行为层:在 bot 已回复过某 finding 的 PR 上,后续轮次以相同措辞重新 decline 同一 finding 时,日志输出 identical bot reply already on the thread,不会重复发布。
  3. 降级:线程抓取过期/为空时(该块设计上就是 best-effort),回复照发——不会因该门卡住任何轮次。

已完成的本地验证:pin 测试通过(含新场景);完整 pin 套件 178/180 通过,2 个失败在干净 origin/main 基线上复现(bite-check 与 verification-gate 测试,与本改动无关);工作流 YAML 解析通过,yamllint 1.35.1(CI 固定版本)零问题;提取的回复块通过 bash -n 与 error 级 shellcheck;去重 jq 表达式用 7 种边界 fixture 测试(bot 完全匹配→跳过;不同内容→发布;人类同内容→发布;旧形态无 author/body→发布;author 为 null→发布;孤儿评论 id→发布;空线程视图→发布)。

证据(改动前后)

改动前:2026-08-16——同一条 finding 回复在几秒内被发布三遍(#9296)。改动后:第二次及之后的相同发布在门处被跳过;首次发布与任何内容变更的后续回复照常落地。

测试环境

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

N/A——仅工作流变更;工作流运行在 ubuntu-latest。

风险与范围

  • 主要风险或权衡:门按 bot 登录名 + 逐字相等的 neutralised 正文比较,因此恰好复述相同 decline 的新轮次会被跳过——这是预期行为,因为 thread 上已经有了;任何新措辞照常发布。线程抓取略微变大(每条评论多 author + body),受既有 first-100 上限约束。
  • 未验证 / 超出范围:Qwen Autofix: review-event storms and duplicate address dispatch waste runner capacity #9296 剩余 P2 项(cron 组语义、resolve-pr 诊断)不在本 PR;本工作流之外的流程(如本地会话)发的回复不受此门覆盖。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

属于 #9296(第二个 P1 项——finding 回复)。不使用关闭关键字——该 issue 还跟踪 P2 项。

A crash-and-rerun of an address round, a same-run repair that regenerates the dispositions, or a later round re-declining the same finding all reproduce the same comment-replies.json entry — and the reply step posted it again, landing identical bot replies on one thread (observed 2026-08-16: one identical reply posted three times, QwenLM#9296).

The thread fetch now also reads each comment's author and body, and the reply step skips posting when the thread already carries a comment by the autofix bot whose body equals the neutralised body about to be posted. A changed body — new information from a later round — still posts; a threads view without author/body, or a stale/empty one, degrades to the old post-always behavior. The replies API itself is already the no-review-event path, so this PR only adds the missing idempotence (the P1 replies item of QwenLM#9296).

Refs QwenLM#9296
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this is an observed bug, not theoretical hardening — the description points to a concrete incident (2026-08-16, one identical finding reply posted three times on a merged PR within seconds, tracked in #9296), with a clear mechanism: a crash-and-rerun, a same-run repair regenerating dispositions, or a later round re-declining an unchanged finding each re-enter the same reply into comment-replies.json and re-post it. Real, reproduced spam.

Direction: aligned. The reply path already uses POST .../comments/{id}/replies, so the only missing piece is the idempotence gate; this adds exactly that and nothing else. Within scope for the autofix workflow.

Size: not applicable — no core-package paths are touched. This is a CI-workflow change (.github/workflows/qwen-autofix.yml) plus its pinned test, ~110 changed lines total.

Approach: minimal and focused. The diff (1) extends the review-threads GraphQL fetch with author{login} body, and (2) skips posting when the thread already carries a bot comment whose body equals the neutralised body about to be posted. A changed body still posts; a stale/empty threads view degrades to the old post-always behavior. No drive-by edits, no scope creep.

Risk: no elevated risk signals — neither changed file matches the high-risk revert-correlated paths.

One process note for later: the branch currently conflicts with main (mergeable state dirty) and the main CI suite hasn't produced a run on this head SHA yet — I'll detail both in the review/verdict comments.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是已观测到的 bug,而非理论性加固——描述指向一起具体事件(2026-08-16,同一条 finding 回复在几秒内被重发三次,见 #9296),机制也清楚:crash 重跑、same-run repair 重新生成 disposition、或后续轮次对未变化的 finding 重新 decline,都会把同一条回复重新写入 comment-replies.json 并重发。是真实发生过的刷屏。

方向:对齐。回复路径本来就用 POST .../comments/{id}/replies,缺的只是幂等门;本 PR 恰好只补上这一点。在 autofix 工作流的范围内。

规模:不适用——未触及核心包路径。这是 CI 工作流改动(.github/workflows/qwen-autofix.yml)加其 pin 测试,共约 110 行。

方案:最小且聚焦。diff(1)在 review-threads 的 GraphQL 抓取里补上 author{login} body;(2)当 thread 上已存在正文与即将发布的 neutralised 正文逐字相等的 bot 评论时跳过发布。正文变了照常发布;线程视图过期/为空时退化为旧的照发行为。无顺手改动,无范围蔓延。

风险:无升级风险信号——两个改动文件都不命中与 revert 相关的高风险路径。

一个后续要注意的流程问题:当前分支与 main 存在冲突(mergeable 状态为 dirty),且主 CI 套件尚未在这个 head SHA 上产生过运行——我会在审查/结论评论里详述这两点。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 5925320ec1091a591647ad483dfa85a8f070b703 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I read the title + "Why it's needed" first and formed my own plan: to stop a re-run re-posting an identical bot reply, the cleanest fix is to read the thread's existing comments before posting and skip when the bot has already left the exact same body — which means the threads fetch has to carry author + body. That's precisely what this PR does, so the approach matches my independent baseline; I didn't find a simpler path it missed. Reading the thread (source of truth) is more robust than persisting a "what I posted" state file across rounds, which would be the fragile alternative.

The implementation checks out:

  • Gate logic is sound. jq -e + any() returns 0 only on a real match, so if … then skip / continue fires only when a bot comment with an equal body is present. (.author.login // "") and (.body // "") make the match null-safe, and a stale/empty THREADS_JSON yields no match and degrades to post-always — consistent with the block's best-effort contract.
  • Integration is correct. The gate lands inside the while … done < <(jq …) reply loop where REPLY_BODY, root_id, THREADS_JSON, and AUTOFIX_BOT are all in scope, so continue cleanly skips to the next reply. It selects the thread by root_id (the actual reply target) and compares against the exact neutralised REPLY_BODY that would be posted — so a CHANGED body still posts, and the same body last posted by a HUMAN is not treated as a duplicate.
  • The diff is the minimal set. Two hunks in the workflow (fetch fields + gate) and the matching pinned-test extension; nothing unrelated.

No critical blockers, no security issues, no AGENTS.md violations. The test change adds exactly the right scenarios (identical bot reply → skipped, changed body → posted, human echo → posted, legacy author/body-less nodes → tolerated).

Non-blocking observation: the dedup only sees the fetched view (first 100 threads / first 100 comments each), so an identical reply beyond that page would re-post. That's the documented, acceptable degradation, not a defect.

Test evidence — the PR's own CI

Check Conclusion
precheck-pr / precheck ✅ success
label ✅ success
authorize ✅ success
verify ⏭️ skipped
tmux-testing ⏭️ skipped
review-config ⏭️ skipped
publish-verify ⏭️ skipped
publish-tmux ⏭️ skipped
triage (this run) 🔄 in_progress

Every check above is a pull_request_target bot-orchestration job. The main Qwen Code CI suite — the one that runs the pinned qwen-autofix-workflow.test.js — has not produced a run on this head SHA (there is no pull_request-event run on this branch at all). So the pinned-test pass quoted in the PR description is the author's self-reported result, not CI evidence — I don't run PR code myself. Not verified: the pinned suite going green on this commit, because CI hasn't executed it here. The finalize job will rewrite the table above once CI settles; if no run appears, that itself is worth a look.

On the sandboxed lanes: the central claim here is CI-workflow shell behavior, not a product build or a TUI surface, so /verify (A/B against a base build) and /tmux (drives the TUI) aren't the right oracle. The purpose-built oracle is the pinned test that extracts the real reply block and drives it against a stubbed gh — which is exactly what needs a CI run to count.

中文说明

代码审查

我先只读标题和"为什么需要",形成自己的方案:要阻止重跑时重发相同的 bot 回复,最干净的修法是在发布前读取 thread 已有评论,当 bot 已留下逐字相同的正文时跳过——这要求线程抓取带上 author + body。这正是本 PR 的做法,与我的独立基线一致,没有找到更简的遗漏路径。读 thread(事实来源)比跨轮次持久化"我发过什么"状态文件更稳健,后者是脆弱的替代方案。

实现经核对无误:

  • 门逻辑正确。 jq -e + any() 只在真正命中时返回 0,因此 if … then 跳过 / continue 只在存在同正文 bot 评论时触发。(.author.login // "")(.body // "") 使匹配对 null 安全;过期/为空的 THREADS_JSON 不会命中,退化为照发——与该块的 best-effort 约定一致。
  • 集成正确。 门落在 while … done < <(jq …) 回复循环内,REPLY_BODYroot_idTHREADS_JSONAUTOFIX_BOT 均在作用域内,continue 干净地跳到下一条回复。它按 root_id(真实回复目标)选定 thread,并与即将发布的、已 neutralise 的 REPLY_BODY 逐字比较——因此变了的正文照常发布,而人类最近发的相同正文不会被当作重复。
  • diff 是最小集合。 工作流两处(抓取字段 + 门)加对应 pin 测试扩展;无无关改动。

无关键阻塞、无安全问题、无 AGENTS.md 违规。测试改动补齐了恰当的场景(相同 bot 回复→跳过;正文变化→发布;人类复读→发布;缺 author/body 的旧节点→容忍)。

非阻塞观察:去重只看抓取到的视图(前 100 个 thread / 每个前 100 条评论),超出该页的相同回复仍会重发。这是已记录、可接受的退化,不是缺陷。

测试证据 —— PR 自己的 CI

上表全部是 pull_request_target 的 bot 编排任务。Qwen Code CI 套件——即运行 pin 测试 qwen-autofix-workflow.test.js 的那个——尚未在这个 head SHA 上产生运行(该分支上根本没有 pull_request 事件的运行)。因此 PR 描述里引用的 pin 测试通过是作者自报结果,不是 CI 证据——我本人不运行 PR 代码。未验证:该 pin 套件在本 commit 上为绿,因为 CI 尚未在此执行它。finalize 任务会在 CI 落定后重写上表;如果一直没有运行出现,这本身就值得排查。

关于沙箱通道:本 PR 的核心主张是 CI 工作流 shell 行为,不是产品构建或 TUI 界面,所以 /verify(对 base 构建做 A/B)与 /tmux(驱动 TUI)都不是合适的神谕。专门的神谕是那个从工作流提取真实回复块、用 stub 的 gh 驱动的 pin 测试——它正需要一次 CI 运行才算数。

Qwen Code · qwen3.8-max

Reviewed at 5925320ec1091a591647ad483dfa85a8f070b703 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @wenshao — the review is clean, but two things I can't resolve from the diff are blocking approval, and both need an author/maintainer hand:

  1. Merge conflict with main (mergeable state dirty) — the branch needs a rebase before it can merge, and that will move the head SHA.
  2. No main-CI run on this commit — there's no pull_request-event run on the branch, so the pinned qwen-autofix-workflow.test.js pass is self-reported, not CI-verified. The orchestration checks don't execute the pinned suite. I can't tell from here whether CI is waiting on approval or on the conflict clearing; either way a maintainer needs to confirm it runs.

Nothing here is a code-quality objection — the change is minimal, correct, and test-pinned. Rebasing onto main and getting the suite green on the new head (approving/triggering CI if it's gated) will move this forward; a @qwen-code /triage re-run will then pick it back up. Needs a human call on the rebase + CI.

中文说明

⏸️ 转交 @wenshao —— 审查是干净的,但有两点我无法从 diff 解决、且需要作者/维护者出手,阻止了批准:

  1. main 存在合并冲突(mergeable 状态 dirty)——分支需要先 rebase 才能合入,而这会移动 head SHA。
  2. 该 commit 没有主 CI 运行——分支上没有 pull_request 事件的运行,因此 pin 测试 qwen-autofix-workflow.test.js 的通过是自报的,未经 CI 验证。编排检查不执行 pin 套件。我无法从这里判断 CI 是在等待批准还是在等冲突解除;无论如何需要维护者确认它能跑起来。

这里没有任何代码质量上的异议——改动最小、正确、且有测试 pin。rebase 到 main、并让套件在新的 head 上变绿(如有门控则批准/触发 CI)即可推进;随后 @qwen-code /triage 重新运行即可继续。rebase 与 CI 一事需要人工定夺。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the code review itself is clean and I'd merge the change on merit, but a merge conflict with main and a missing main-CI run on this commit block approval right now.

Stepping back: the problem is real and observed (the 2026-08-16 triple-post in #9296), not a hypothetical. The approach matches what I'd have proposed independently — read the thread's existing comments and skip an identical bot reply — and I didn't find a simpler path it missed. The diff is the minimal set: two workflow hunks plus the matching pinned-test scenarios, no drive-by edits, and the degradation (post-always on a stale/empty view) is deliberate and documented. In six months this reads as a small, well-commented, test-pinned gate — I'd thank the author, not curse them. I'm not approving out of momentum; I'm holding for two concrete, checkable reasons below.

Why not approve yet:

  1. Merge conflict. The branch is CONFLICTING with main (mergeable state dirty). It can't be merged as-is, and a rebase moves the head SHA — any approval I give now would be dismissed then anyway. This needs an author action, not a review judgment.
  2. No main-CI run on this commit. There is no pull_request-event run on this branch, so the pinned qwen-autofix-workflow.test.js pass is the author's self-reported result, not CI evidence. The orchestration checks are green, but they don't execute the pinned suite.

Neither is a code-quality concern, and neither is something I can resolve from the diff — hence defer rather than approve or request-changes. No approve-on-green marker: the verdict isn't approve, there's no pending pull_request run to wait on, and the rebase will move the SHA regardless.

中文说明

信心:3/5 —— 代码审查本身是干净的,就改动本身而言我愿意合入,但当前与 main 的合并冲突以及该 commit 上缺失的主 CI 运行阻止了批准。

退一步看:问题是真实且已观测到的(#9296 里 2026-08-16 的三连发),不是假设。方案与我独立提出的一致——读取 thread 已有评论、跳过相同的 bot 回复——我也没找到它遗漏的更简路径。diff 是最小集合:工作流两处加对应的 pin 测试场景,无顺手改动;退化行为(视图过期/为空时照发)是有意且已记录。六个月后回看,这是一个小而注释充分、有测试 pin 的门——我会感谢作者而非抱怨。我不是因为没理由拒绝才批准;而是因为下面两个具体、可核查的原因而暂缓。

为何暂不批准:

  1. 合并冲突。 分支与 main 处于 CONFLICTING(mergeable 状态 dirty),当前无法合入;rebase 会移动 head SHA——我现在给的任何批准到时也会被驳回。这需要作者操作,而非评审判断。
  2. 该 commit 没有主 CI 运行。 该分支上没有 pull_request 事件的运行,因此 pin 测试 qwen-autofix-workflow.test.js 的通过是作者自报结果,不是 CI 证据。编排检查是绿的,但它们不执行 pin 套件。

两者都不是代码质量问题,也不是我能从 diff 里解决的——所以是暂缓(defer),而非批准或要求修改。未放置 approve-on-green 标记:结论不是批准,也没有待定的 pull_request 运行可等,且 rebase 无论如何都会移动 SHA。

Qwen Code · qwen3.8-max

Reviewed at 5925320ec1091a591647ad483dfa85a8f070b703 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread .github/workflows/qwen-autofix.yml Outdated
repository(owner:$owner,name:$name){
pullRequest(number:$pr){
reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId}}} pageInfo{hasNextPage}}
reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId author{login} body}}} pageInfo{hasNextPage}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new author{login} body fields that supply the idempotence gate are pinned by no test. Measured with a revert probe: reverting only this query hunk (gate and new tests kept) leaves all 180 tests green — the reply test injects THREADS_JSON directly via env, and the mock gh shim matches the query only on its reviewThreads(first:100) prefix, so the field list is never exercised. If a later edit to this 7.6k-line workflow drops author{login} or body here — a simplification, or a copy from one of the file's other GraphQL queries — it passes every test, and in production every thread node then has null author/body, the gate never matches, and the duplicate-reply regression this PR fixes (#9296) returns with no test red. Fix: pin the supplier — extend the mock gh shim to require the threads query contains both author{login} and body before answering, e.g. extend the [[ "$query" == *"reviewThreads(first:100)"* ]] branch to also require *"author{login}"* and *"body"*, exiting 2 otherwise (the shim already uses that convention for other query checks).

中文说明

为幂等门提供数据的新字段 author{login} body 没有任何测试保护。回退探针实测:只回退这一行查询(保留门和新测试),全部 180 个测试仍然绿——回复测试通过环境变量直接注入 THREADS_JSON,mock gh 脚本只按 reviewThreads(first:100) 前缀匹配查询,字段列表从未被真正验证。如果之后有人在这个 7.6k 行的工作流里删掉这里的 author{login}body(简化,或从文件里其他 GraphQL 查询复制而来),所有测试照样通过;而生产环境里每个 thread 节点的 author/body 都会是 null,门永远匹配不上,本 PR 要修的重复回复回归(#9296)会在没有任何测试变红的情况下复活。修复:把供应端钉住——让 mock gh 脚本在应答前校验线程查询同时包含 author{login}body,例如在 [[ "$query" == *"reviewThreads(first:100)"* ]] 分支里追加要求 *"author{login}"**"body"*,否则 exit 2(该脚本对其他查询检查已用同样约定)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread .github/workflows/qwen-autofix.yml Outdated
repository(owner:$owner,name:$name){
pullRequest(number:$pr){
reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId}}} pageInfo{hasNextPage}}
reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId author{login} body}}} pageInfo{hasNextPage}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The gate can only ever see the first 100 comments of a thread: comments(first:100) fetches no pageInfo, so per-thread comment truncation is both invisible and silent. The fetch above warns on the 100-thread cap (hasNextPage), but nothing warns when a thread's comment list is cut — the payload cannot even detect it today. On long-lived autofix PRs the same thread grows with every round (the #9296 shape — a repeatedly re-posted declination — is exactly such a thread); once a stored bot reply sits past comment 100, a crash-and-rerun or later round regenerating the unchanged declination finds no match and re-posts the duplicate — the exact symptom this change fixes — with no log signal. Suggested fix: fetch comments(first:100){nodes{...} pageInfo{hasNextPage}} and emit the same ::warning:: when any thread's comments are truncated, so the degraded post-always behavior is observable — matching the precedent the thread-cap warning already sets here:

reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}} pageInfo{hasNextPage}}
中文说明

门最多只能看到一个 thread 的前 100 条评论:comments(first:100) 没有取 pageInfo,因此单 thread 评论截断既不可见、也无日志。上方的抓取只对 100 个线程的上限(hasNextPage)告警,线程内评论列表被截断时没有任何告警——目前的 payload 甚至无法检测到这一点。在长期运行的 autofix PR 上,同一个 thread 会随每轮增长(#9296 的形态——同一条 decline 被反复重发——正是这种 thread);一旦已存的 bot 回复落在第 100 条之后,crash 重跑或后续轮次重新生成相同措辞时找不到匹配,就会再次重发重复回复——正是本改动要修的症状——且没有任何日志信号。建议修复:抓取 comments(first:100){nodes{...} pageInfo{hasNextPage}},并在任一线程评论被截断时发出同样的 ::warning::,让退化为“照发”的行为可观测——与这里线程上限告警已有的先例保持一致:

reviewThreads(first:100){nodes{id isResolved comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}} pageInfo{hasNextPage}}

— qwen3.8-max via Qwen Code /review (v0.21.14)

const deduped = readFileSync(repliedLog, 'utf8').trim().split('\n');
expect(deduped).toHaveLength(1);
expect(deduped[0]).toContain('pulls/7731/comments/100/replies');
expect(deduped[0]).toContain('body=Changed reason');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] None of the new fixture bodies contains <!--, so a one-line mutant — the gate comparing the raw decoded body instead of the neutralised REPLY_BODY — survives the suite (measured: mutant applied, 1 passed | 179 skipped). In production, reply bodies containing <!-- are neutralised to <!\-\- before posting (the block's own comment anticipates model output smuggling control markers); a later round regenerating the same raw body would then compare raw against the stored neutralised form, miss the match, and repost the duplicate. The code as written compares the neutralised body correctly — only the pin is missing. Fix: add one scenario where the pending reply body contains <!-- and the existing bot comment's body holds the neutralised <!\-\- form, and assert the skip.

中文说明

新增 fixture 的正文都不含 <!--,因此一个单行变异体——门改为比较原始解码正文而非 neutralised 后的 REPLY_BODY——能在整个测试套件下存活(实测:应用变异体后 1 passed | 179 skipped)。生产环境中,含 <!-- 的回复正文在发布前会被中和为 <!\-\-(该块自己的注释已预料到模型输出可能夹带控制标记);后续轮次重新生成相同原始正文时,会拿原始形式去比对已存的中和形式,匹配失败,重复回复再次发出。当前代码的比较是正确的——缺的只是把这个契约钉住的测试。修复:新增一个场景——待发回复正文含 <!--,thread 上已有的 bot 评论正文为中和后的 <!\-\- 形式,断言跳过发布。

— qwen3.8-max via Qwen Code /review (v0.21.14)

},
]);
const humanEcho = readFileSync(repliedLog, 'utf8').trim().split('\n');
expect(humanEcho).toHaveLength(1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Every fixture uses exactly one thread, so a mutant scanning comments across ALL threads (instead of scoping to the thread containing the target id) also survives the suite (measured: cross-thread-scan mutant, 1 passed | 179 skipped). In production that mutant is harmful: the bot posts decline reason X on thread A in one round (short templated reasons can repeat across findings), and a later round's pending reply with the same body X aimed at thread B gets suppressed because thread A carries it — thread B's reviewer sees exactly the silence this block exists to prevent. The code as written scopes correctly — only the pin is missing. Fix: add a two-thread fixture where thread A carries a bot comment with body X and a pending reply with the same body X targets thread B; assert the reply on B still posts.

中文说明

所有 fixture 都只有一个线程,因此一个跨所有线程扫描评论(而不是限定在包含目标 id 的线程内)的变异体同样能在套件下存活(实测:跨线程扫描变异体,1 passed | 179 skipped)。在生产中该变异体有害:bot 在某轮于线程 A 发布了 decline 理由 X(简短的模板化理由可能在不同 finding 间重复),后续轮次一条正文同为 X 的待发回复指向线程 B 时,会因为线程 A 已有该内容而被跳过——线程 B 的 reviewer 看到的正是本块要防止的“沉默”。当前代码的作用域是正确的——缺的只是钉住它的测试。修复:新增双线程 fixture——线程 A 上已有 bot 评论正文 X,正文同为 X 的待发回复指向线程 B,断言 B 上的回复照常发布。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment on lines +16381 to +16382
databaseId: 300,
author: { login: 'qwen-code-dev-bot' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] In both new fixtures the compared comment (bot or human) is the LAST node of its thread, so a mutant narrowing the gate's scan to the newest comment — (.[0].comments.nodes // [])[-1:] — passes the whole suite (measured: 180/180 green with the mutant). The common real thread shape is root comment, bot declination X, then a human reply answering it; on that shape a crash-and-rerun regenerating the identical disposition X meets a newest-comment-only gate that sees only the human's comment, finds no match, and re-posts X — the #9296 duplicate returns on the most common thread shape while every test stays green (probe-verified: original gate MATCH/skip, mutant NO MATCH/repost). Fix: add one fixture where a human (or authorless) comment FOLLOWS the bot's matching comment in the same thread, and assert the reply is still skipped.

中文说明

两个新 fixture 里被比对的评论(bot 或人类)都是各自线程的最后一个节点,因此把门的扫描收窄到最新一条评论的变异体——(.[0].comments.nodes // [])[-1:]——能通过整个套件(实测:变异体下 180/180 全绿)。真实线程的常见形态是:根评论、bot 的 decline X、随后人类回复作答;在这种形态下,crash 重跑重新生成相同 disposition X 时,只看最新评论的门只会看到人类那条,匹配失败,X 被再次发布——#9296 的重复回复在最常见线程形态上复活,而所有测试保持绿(探针验证:原门 MATCH/跳过,变异体 NO MATCH/重发)。修复:新增一个 fixture,让一条人类(或无作者)评论跟在该线程中 bot 的匹配评论之后,断言回复仍被跳过。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment on lines +16382 to +16383
author: { login: 'qwen-code-dev-bot' },
body: 'Deferred — follow-up.\n\n中文:已延后。',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No reply-gate fixture sets isResolved on a thread, so a mutant adding select(.isResolved | not) to the gate's selector — a plausible harmonisation with the resolve block's selector ~15 lines above in the workflow, which carries exactly that filter — survives the entire suite (measured: 180/180 green with the mutant; probe on a resolved thread flips MATCH→NO MATCH). The harm path is real: after a re-arm marker resets the watermark the next round re-reads all feedback, and the REST pulls/{pr}/comments enumeration carries no resolved state — so the agent can legitimately re-decline an old finding whose thread was resolved after the bot's first reply, and an isResolved-filtered gate would repost the duplicate onto the resolved thread. The current code is correct (the gate ignores isResolved); only the pin is missing. Fix: add isResolved: true to the dedupe scenario's thread object (or add a scenario) and keep the skip assertions.

中文说明

没有任何回复门的 fixture 给线程设置 isResolved,因此在门的选择器上加 select(.isResolved | not) 的变异体——与工作流上方约 15 行处 resolve 块选择器(恰好带这个过滤条件)的“合理对齐”——能在整个套件下存活(实测:变异体下 180/180 全绿;在已解决线程上的探针显示 MATCH→NO MATCH 翻转)。危害路径真实存在:re-arm 标记重置水位线后,下一轮会重新读取全部反馈,而 REST pulls/{pr}/comments 枚举不携带线程解决状态——因此 agent 完全可能对一条“bot 首次回复后被 reviewer 解决”的旧 finding 重新 decline,而带 isResolved 过滤的门会把重复回复重发到已解决的线程上。当前代码是正确的(门忽略 isResolved);缺的只是钉住这一点的测试。修复:在去重场景的线程对象上加 isResolved: true(或新增场景),并保留跳过断言。

— qwen3.8-max via Qwen Code /review (v0.21.14)

@wenshao

wenshao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code attempted to resolve merge conflicts but the run did not complete successfully.

Check the workflow run for full logs.

…mpotence

# Conflicts:
#	.github/workflows/qwen-autofix.yml

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • .github/workflows/qwen-autofix.yml:6469 — [probe] in-pass double-post: the gate consults only the pre-loop THREADS_JSON snapshot, so two same-thread identical entries in one comment-replies.json both post (unchanged since round 1 — deferred…
  • scripts/tests/qwen-autofix-workflow.test.js:16575 — [probe] no substring-relation fixture: a containment relaxation of the gate's exact-equality survives the whole suite (unchanged since round 1 — deferred by the code-age rule)
中文说明

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread .github/workflows/qwen-autofix.yml Outdated
pullRequest(number:$pr){
reviewThreads(first:100, after:$endCursor){
nodes{id isResolved comments(first:100){nodes{databaseId} pageInfo{hasNextPage}}}
nodes{id isResolved comments(first:100){nodes{databaseId author{login} body}} pageInfo{hasNextPage}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R2-3: This query line moves pageInfo{hasNextPage} OUT of comments(first:100) to the review-thread node level — but pageInfo is not a field of PullRequestReviewThread, so GitHub rejects the entire query. Verified live at this commit: the PR's verbatim query returns Field 'pageInfo' doesn't exist on type 'PullRequestReviewThread' (gh exit 1, error envelope only); the same query with pageInfo{hasNextPage} restored inside comments(...) returns exit 0 with real thread data. Consequence on every run: THREADS_FETCH_OK=falseTHREADS_JSON='[]' → (1) the resolve block resolves nothing — a regression of pre-existing behavior; (2) root_id falls back to rc_id, so replies aimed at reply-ids are rejected by GitHub; (3) the idempotence gate — the point of this PR — always sees an empty view and degrades to post-always, so the #9296 duplicate replies recur every round. All tests stay green because the stub gh matches the query by substring and THREADS_JSON is injected by env. The field was relocated by the merge with main (#9390's pagination rewrite).

Witness (live probe at the reviewed commit):

gh api graphql (PR's verbatim query) -> exit 1
  "Field 'pageInfo' doesn't exist on type 'PullRequestReviewThread'"
control (pageInfo restored inside comments(...)) -> exit 0, real thread data

Fix: restore the inner position — comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}} — and drop the stray thread-level pageInfo{hasNextPage}. Note the >100-comment truncation detector at ~line 6362 consumes .comments.pageInfo.hasNextPage: a repair that only deletes the stray field leaves that warning permanently dead while the suite stays green (the existing pin injects pageInfo via THREADS_RAW_STUB and cannot discriminate the two fix shapes).

中文说明

[Critical] R2-3:该查询行把 pageInfo{hasNextPage}comments(first:100) 内移到了 review-thread 节点层级——但 PullRequestReviewThread 类型没有 pageInfo 字段,GitHub 会拒绝整个查询。已在本 commit 实测验证:PR 原样查询返回 Field 'pageInfo' doesn't exist on type 'PullRequestReviewThread'(gh exit 1,只有错误信封);将 pageInfo{hasNextPage} 恢复到 comments(...) 内的对照查询返回 exit 0 和真实线程数据。每次运行的后果:THREADS_FETCH_OK=falseTHREADS_JSON='[]' →(1)resolve 块什么都不会 resolve——既有行为的回归;(2)root_id 回退为 rc_id,指向回复 id 的回复会被 GitHub 拒绝;(3)幂等门(本 PR 的目的)永远看到空视图、退化为照发,#9296 的重复回复每轮都会复发。所有测试仍然绿色,因为 stub gh 只按子串匹配查询、且 THREADS_JSON 由环境变量注入。该字段是在与 main 的合并(#9390 的分页重写)中被移位的。

修复:恢复内层位置——comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}——并删除错位的线程级 pageInfo{hasNextPage}。注意约 6362 行的 >100 评论截断检测器消费 .comments.pageInfo.hasNextPage:只删除错位字段而不恢复内层字段,会让该警告在套件全绿的情况下永久失效(现有 pin 通过 THREADS_RAW_STUB 注入 pageInfo,无法区分两种修法)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread .github/workflows/qwen-autofix.yml Outdated
pullRequest(number:$pr){
reviewThreads(first:100, after:$endCursor){
nodes{id isResolved comments(first:100){nodes{databaseId} pageInfo{hasNextPage}}}
nodes{id isResolved comments(first:100){nodes{databaseId author{login} body}} pageInfo{hasNextPage}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-1: Still standing at this commit (revert probe re-measured): the author{login} body fields supplying the idempotence gate are pinned by no test. Reverting only this query hunk (gate and new tests kept) leaves all tests green — the reply-gate tests inject THREADS_JSON downstream of the fetch, and the resolve-block test pins only reviewThreads(first:100, after:$endCursor) and the outer pageInfo{hasNextPage endCursor} order, not the comment field list. A later edit trimming the query back to nodes{databaseId} (e.g. for payload size) would leave every test green while the gate silently degrades to post-always in production and the #9296 duplication recurs. Witness: revert probe at this commit → 2 passed | 178 skipped; grep of scripts/tests for comments(first:100) / author{login}: zero pins. Fix: pin the query string next to the existing pins (~line 16117 of the test file), e.g. expect(block).toContain('comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}') — it must nail pageInfo's inner position too (a substring pin of the shipped line would pass while GitHub rejects the query — see the Critical above).

中文说明

[Suggestion] R1-1:在本 commit 仍然存在(revert 探针重新实测):为幂等门提供数据的 author{login} body 字段没有任何测试 pin。只还原这个查询 hunk(保留门和新测试)时所有测试仍绿——回复门测试在 fetch 下游注入 THREADS_JSON,而 resolve 块测试只 pin 了 reviewThreads(first:100, after:$endCursor) 和外层 pageInfo{hasNextPage endCursor} 的顺序,没有 pin 评论字段列表。后续若把查询裁剪回 nodes{databaseId}(例如为了 payload 体积),所有测试仍绿,而生产中门会静默退化为照发,#9296 的重复回复复发。证据:本 commit 上的 revert 探针 → 2 passed | 178 skipped;在 scripts/tests 中 grep comments(first:100) / author{login}:零 pin。修复:在现有 pin 旁(测试文件约 16117 行)pin 查询字符串,例如 expect(block).toContain('comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}')——必须同时钉住 pageInfo 的内层位置(对当前行做子串 pin 会在 GitHub 拒绝查询的情况下仍然通过——见上面的 Critical)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

const deduped = readFileSync(repliedLog, 'utf8').trim().split('\n');
expect(deduped).toHaveLength(1);
expect(deduped[0]).toContain('pulls/7731/comments/100/replies');
expect(deduped[0]).toContain('body=Changed reason');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-3: Still standing at this commit (mutant probe re-measured): none of the new fixture bodies contains <!--, so a one-line mutant — the gate comparing the raw decoded body instead of the neutralised REPLY_BODY — survives the suite (1 passed | 179 skipped with the mutant applied). A future edit comparing the pre-neutralisation body would silently stop matching stored (neutralised) bodies in production, and duplicates would recur with the suite green. Probe flip verified: a fixture whose reply body contains <!-- fails the mutant (duplicate posted) and passes shipped code (skip logged). Fix: add such a fixture, with the stored bot body carrying the sed-neutralised form.

中文说明

[Suggestion] R1-3:在本 commit 仍然存在(突变探针重新实测):新 fixture 的正文都不含 <!--,因此一个单行突变——门比较 base64 解码后的原始正文而非 neutralised 的 REPLY_BODY——能通过整个套件(应用突变后 1 passed | 179 skipped)。未来若把比较改成 neutralise 之前的正文,生产上会与已存储(已 neutralise)的正文永远不匹配,重复回复复发而套件全绿。翻转已验证:回复正文含 <!-- 的 fixture 能让突变失败(重复发布)、对现有代码通过(记录跳过)。修复:新增这样一个 fixture,存储的 bot 正文使用 sed neutralise 之后的形态。

— qwen3.8-max via Qwen Code /review (v0.21.14)

},
]);
const humanEcho = readFileSync(repliedLog, 'utf8').trim().split('\n');
expect(humanEcho).toHaveLength(1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-4: Still standing at this commit (probe re-measured): every reply-gate fixture uses exactly one thread, so a mutant scanning comments across ALL threads (instead of scoping to the thread containing the target id) survives the suite. A/B at this commit: against a two-thread view where thread 1 holds the bot's duplicate body and the reply targets thread 2, the shipped thread-scoped gate exits 1 (post — correct) while the cross-thread mutant exits 0 (skip — wrong). In production that shape silently suppresses finding B's reply because finding A's thread already carries the same declination — exactly the silence this block exists to prevent. Fix: add a two-thread fixture asserting the reply still posts to thread 2's root while a same-body reply to thread 1 is skipped.

中文说明

[Suggestion] R1-4:在本 commit 仍然存在(探针重新实测):所有回复门 fixture 都只有一个线程,因此跨所有线程扫描评论(而不是限定在包含目标 id 的线程内)的突变能通过套件。本 commit 上的 A/B:在「线程 1 存有 bot 的相同正文、回复目标是线程 2」的双线程视图下,现有的线程限定门 exit=1(发布——正确),跨线程突变 exit=0(跳过——错误)。生产中这种形态会因为 finding A 的线程已有相同措辞而静默吞掉 finding B 的回复——正是本块要防止的静默。修复:新增双线程 fixture,断言回复仍会发往线程 2 的根评论,同时对线程 1 的同正文回复被跳过。

— qwen3.8-max via Qwen Code /review (v0.21.14)

{ databaseId: 222 },
{
databaseId: 300,
author: { login: 'qwen-code-dev-bot' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-5: Still standing at this commit (probe re-measured): in both new fixtures the compared comment (bot or human) is the LAST node of its thread, so a mutant narrowing the gate's scan to the newest comment survives the whole suite. A/B at this commit: with the matching bot comment followed by a later unrelated human comment in the same thread, the shipped gate exits 0 (skip — correct) while the newest-only mutant exits 1 (post — duplicate). Fix: add a fixture where the matching bot comment is followed by another comment in the same thread.

中文说明

[Suggestion] R1-5:在本 commit 仍然存在(探针重新实测):两个新 fixture 中被比较的评论(bot 或人类)都是其线程的最后一个节点,因此把门的扫描收窄到最新评论的突变能通过整个套件。本 commit 上的 A/B:匹配的 bot 评论之后同一线程还有一条无关的人类评论时,现有门 exit=0(跳过——正确),只看最新评论的突变 exit=1(发布——重复)。修复:新增一个 fixture,让匹配的 bot 评论之后同线程还有另一条评论。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment on lines +16587 to +16588
author: { login: 'qwen-code-dev-bot' },
body: 'Deferred — follow-up.\n\n中文:已延后。',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-6: Still standing at this commit (probe re-measured): no reply-gate fixture sets isResolved on a thread, so a mutant adding select(.isResolved | not) to the gate's selector — a plausible harmonisation with the resolve block's selector ~15 lines above in the workflow — survives the suite (null | not is true when the field is absent). With that mutant in place, a reply to a comment in a resolved thread is silently skipped (no post, no distinct log line) and the suite stays green — the intended semantics for resolved threads are unpinned. Fix: add a fixture with isResolved: true on the thread asserting the reply still posts (or is deliberately skipped with a distinct log line, per the intended semantics).

中文说明

[Suggestion] R1-6:在本 commit 仍然存在(探针重新实测):没有任何回复门 fixture 给线程设置 isResolved,因此在门的选择器上加 select(.isResolved | not) 的突变能通过套件——这是一种与工作流上方约 15 行 resolve 块选择器的「合理对齐」,且字段缺失时 null | not 为 true。该突变生效后,对已 resolve 线程中评论的回复会被静默跳过(不发布、也没有独立日志),套件仍全绿——resolved 线程下的预期语义没有被 pin。修复:新增一个线程带 isResolved: true 的 fixture,断言回复仍会发布(或按预期语义被有意跳过并输出独立日志)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

@wenshao

wenshao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

1 similar comment
@wenshao

wenshao commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 20, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 1 finishedview run. See this round's report below.

中文说明

AutoFix 第 1 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #9463 (address-review)

The round-2 review was CHANGES_REQUESTED with one Critical (R2-3) plus six re-raised Suggestions (R1-1…R1-6). All 12 inline findings are addressed and resolved in code in one commit: d4dcfd056c (+82/−1 across the workflow and its test file).

Critical R2-3 — threads query rejected by GitHub (rc:3813955281)

Fixed. The merge with main (#9390's pagination rewrite) relocated pageInfo{hasNextPage} OUT of comments(first:100) to the review-thread node level; PullRequestReviewThread has no pageInfo field, so GitHub rejects the entire query (the reviewer verified live: Field 'pageInfo' doesn't exist on type 'PullRequestReviewThread'). Confirmed structurally in the code: the brace placement put pageInfo at the thread level and left comments(...) without it. Consequence on every run was THREADS_FETCH_OK=falseTHREADS_JSON='[]': the resolve block resolved nothing, root_id fell back to the raw id, and the idempotence gate — the point of this PR — degraded to post-always.

Fix: restored pageInfo{hasNextPage} inside comments(...) and dropped the stray thread-level field, exactly the control shape the reviewer's live probe confirmed returns real thread data:

comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}

R1-2 — per-thread comment truncation invisible (rc:3810982938)

Resolved by the same query fix. The >100-comment truncation detector (consumes .comments.pageInfo.hasNextPage, emits the ::warning::) already exists from #9390's pagination rewrite, but stayed dead while the query was rejected. Restoring the inner pageInfo makes it live; the resolve test's existing deepThread/cleanRun scenarios pin the warning's presence and absence. No new code needed.

R1-1 — gate-supplying fields pinned by no test (rc:3810982933, rc:3813955286)

Fixed. Added a substring pin next to the existing query pins in the resolve test: expect(block).toContain('comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}'). It kills the measured revert probe (dropping author{login}/body) AND nails pageInfo's inner position — the pin does not match the hoisted (GitHub-rejected) shape. Verified reproduce-first: the pin fails on the pre-fix line and passes after the fix.

R1-3 — neutralised-body comparison unpinned (rc:3810982942, rc:3813955292)

Fixed. Added a scenario where the pending reply body contains <!\-\- and the thread already carries the bot comment with the sed-neutralised <!\-\- form; asserts the reply is skipped. A mutant comparing the raw decoded body would post the duplicate and fail the assertion.

R1-4 — thread-scoping unpinned (rc:3810982947, rc:3813955301)

Fixed. Added a two-thread scenario: thread 1 carries the bot comment with body X; a pending reply with the same body X targets thread 2. Asserts the reply on thread 2 still posts (remapped to that thread's root 400) while the same-body reply to thread 1 is skipped. A cross-thread-scan mutant would suppress thread 2's reply and fail.

R1-5 — full-comment scan unpinned (rc:3810982949, rc:3813955308)

Fixed. The dedupe scenario's thread now has a human comment FOLLOWING the matching bot comment in the same thread (the common live shape: root, bot declination, human answer). The skip assertions still hold; a newest-comment-only mutant would re-post the duplicate and fail.

R1-6 — isResolved semantics unpinned (rc:3810982953, rc:3813955314)

Fixed. The dedupe scenario's thread now carries isResolved: true. The gate deliberately ignores resolution state — after a re-arm a round can legitimately re-decline a finding whose thread a reviewer resolved after the bot's first reply — so the skip assertions still hold and pin that semantics; a select(.isResolved | not) mutant on the gate's selector would re-post onto the resolved thread and fail.

Not requested this round — reviewer's own deferrals

The round-2 review body records two probe findings as "Deferred under the convergence posture … recorded, not requested in this round" (in-pass double-post through the pre-loop THREADS_JSON snapshot; no substring-containment fixture). Left as-is per that explicit deferral; they remain tracked in the reviewer's ledger and were not implemented or declined here.

Issue-level comment ic:5338263958 (deferring to @wenshao)

The merge-conflict half is stale: HEAD (4ef415b011) already merged origin/main, and this round was invoked with --conflict false. The no-main-CI half resolves once this round's commit is pushed (CI triggers on the push). No code action available for either.

Conflict notes

--conflict false — no merge performed this round.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow.test.js -t "(resolves only the review threads|answers the threads it leaves open)" against pre-fix code — 1 failed | 1 passed (the new query pin reproduces the defect on the current code; the gate fixtures pass, confirming the gate logic itself is correct)
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow.test.js after the fix — 180 passed (180), exit 0. One vitest worker-RPC unhandled error ([vitest-worker]: Timeout calling "onTaskUpdate") reproduces with this round's two tests excluded (178 passed | 2 skipped, same 1 error) — pre-existing environmental noise in this long suite, unrelated to the change
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, 0 errors)
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — passed
  • YAML parse of .github/workflows/qwen-autofix.yml (js-yaml) — valid
  • A live GraphQL probe is not possible from this runner (no GitHub credentials); the fix restores exactly the control query shape the reviewer's live probe measured returning exit 0 with real thread data
  • Integration tests after npm run bundle — not run: the touched behavior lives entirely in workflow YAML and its script test, never in the bundled CLI. npm run generate:settings-schema — not run: no settings source changed
中文说明

本轮总结 — PR #9463(address-review)

第 2 轮审查为 CHANGES_REQUESTED,含 1 个 Critical(R2-3)与 6 个重新提出的 Suggestion(R1-1…R1-6)。全部 12 条行内 finding 均已处理并在代码中解决,合入一个 commit:d4dcfd056c(工作流与测试文件共 +82/−1)。

Critical R2-3 — 线程查询被 GitHub 拒绝(rc:3813955281)

已修复。 与 main 的合并(#9390 的分页重写)把 pageInfo{hasNextPage}comments(first:100) 内移到了 review-thread 节点层级;PullRequestReviewThread 类型没有 pageInfo 字段,GitHub 会拒绝整个查询(审查者已实测:Field 'pageInfo' doesn't exist on type 'PullRequestReviewThread')。已在代码中结构性确认:花括号位置使 pageInfo 落在线程层级,而 comments(...) 内没有它。每次运行的后果是 THREADS_FETCH_OK=falseTHREADS_JSON='[]':resolve 块什么都不会 resolve,root_id 回退为原始 id,而幂等门——本 PR 的目的——退化为照发。

修复:将 pageInfo{hasNextPage} 恢复到 comments(...) 内,并删除错位的线程级字段,与审查者实测确认能返回真实线程数据(exit 0)的对照查询完全一致:

comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}

R1-2 — 单线程评论截断不可见(rc:3810982938)

由同一个查询修复解决。 >100 条评论的截断检测器(消费 .comments.pageInfo.hasNextPage、发出 ::warning::)已随 #9390 的分页重写存在,但在查询被拒绝期间一直是死代码。恢复内层 pageInfo 后即生效;resolve 测试中已有的 deepThread/cleanRun 场景分别钉住了该警告的出现与缺席。无需新代码。

R1-1 — 为门提供数据的字段无测试保护(rc:3810982933、rc:3813955286)

已修复。 在 resolve 测试中现有查询 pin 旁新增子串 pin:expect(block).toContain('comments(first:100){nodes{databaseId author{login} body} pageInfo{hasNextPage}}')。它既能杀死实测的还原探针(删掉 author{login}/body),又钉住了 pageInfo 的内层位置——该 pin 不会匹配错位的(被 GitHub 拒绝的)形态。已按先复现后修复验证:pin 在修复前的行上失败,修复后通过。

R1-3 — neutralised 正文比较未被钉住(rc:3810982942、rc:3813955292)

已修复。 新增场景:待发回复正文含 <!\-\-,线程上已有 bot 评论的正文为 sed 中和后的 <!\-\- 形式;断言回复被跳过。若变异体改为比较原始解码正文,则会发出重复回复并使断言失败。

R1-4 — 线程作用域未被钉住(rc:3810982947、rc:3813955301)

已修复。 新增双线程场景:线程 1 上已有正文为 X 的 bot 评论;正文同为 X 的待发回复指向线程 2。断言线程 2 上的回复照常发布(重映射到该线程根评论 400),同时指向线程 1 的同正文回复被跳过。跨线程扫描的变异体会吞掉线程 2 的回复并使断言失败。

R1-5 — 全评论扫描未被钉住(rc:3810982949、rc:3813955308)

已修复。 去重场景的线程现在在匹配的 bot 评论之后还有一条人类评论(常见真实形态:根评论、bot 的 decline、人类作答)。跳过断言仍然成立;只看最新评论的变异体会重发重复回复并使断言失败。

R1-6 — isResolved 语义未被钉住(rc:3810982953、rc:3813955314)

已修复。 去重场景的线程现在带 isResolved: true。门有意忽略线程解决状态——re-arm 之后的一轮完全可能对「bot 首次回复后被 reviewer 解决」的 finding 重新 decline——因此跳过断言仍然成立并钉住该语义;在门的选择器上加 select(.isResolved | not) 的变异体会把重复回复重发到已解决的线程上并使断言失败。

本轮不要求处理 — 审查者自身的延后项

第 2 轮审查正文记录了 2 条探针 finding,标注「收敛姿态下延后……已记录,本轮不要求修改」(经由循环前 THREADS_JSON 快照的同轮内双发;缺少子串包含关系的 fixture)。按该明确延后保持原样;它们仍由审查者的 ledger 跟踪,本轮既未实现也未拒绝。

Issue 级评论 ic:5338263958(转交 @wenshao

合并冲突一半已过时:HEAD(4ef415b011)已合并 origin/main,且本轮以 --conflict false 调用。没有主 CI 运行的一半会在本轮 commit 推送后自动解决(CI 由推送触发)。两者均无可执行的代码动作。

冲突说明

--conflict false — 本轮未执行合并。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow.test.js -t "(resolves only the review threads|answers the threads it leaves open)"(修复前的代码)— 1 failed | 1 passed(新的查询 pin 在现有代码上复现了缺陷;门的 fixture 通过,证明门逻辑本身正确)
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow.test.js(修复后)— 180 passed (180),exit 0。出现 1 个 vitest worker-RPC 未处理错误([vitest-worker]: Timeout calling "onTaskUpdate"),在排除本轮两个测试后同样复现(178 passed | 2 skipped,同样 1 个错误)——属于该长套件中预先存在的环境噪声,与本改动无关
  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0,0 错误)
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — 通过
  • .github/workflows/qwen-autofix.yml 的 YAML 解析(js-yaml)— 有效
  • 本 runner 无法做 GraphQL 实测(无 GitHub 凭据);修复恢复的正是审查者实测返回 exit 0 和真实线程数据的对照查询形态
  • npm run bundle 后的集成测试 — 未运行:改动的行为完全在工作流 YAML 及其脚本测试中,从不经由打包后的 CLI。npm run generate:settings-schema — 未运行:未改动任何 settings 源

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues. LGTM! ✅

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • .github/workflows/qwen-autofix.yml:5478 — [review] gate comment attributes the 2026-08-16 #9296 incident to a flow this gate structurally cannot catch (human-authored, cross-thread, standalone review submissions)
中文说明

无阻断问题。LGTM!✅

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

@wenshao
wenshao enabled auto-merge August 20, 2026 12:20

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. CI green. The reply-idempotence gate is correctly scoped to exact duplicates (same bot login + byte-identical body), so a later round with a changed reason still posts; it degrades to post-always on a stale/empty threads view rather than silently dropping replies. Tests pin the GraphQL field shape (author/body + pageInfo's inner position) and the fall-back to a pre-existing authorless/bodyless thread view.

@wenshao
wenshao added this pull request to the merge queue Aug 20, 2026
Merged via the queue into QwenLM:main with commit c59910b Aug 20, 2026
57 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.15.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants