Skip to content

test(e2e): isolate protocol checks and ACP test state - #11286

Merged
yiliang114 merged 8 commits into
mainfrom
autofix/issue-11271
Sep 8, 2026
Merged

yiliang114 merged 8 commits into
mainfrom
autofix/issue-11271

Conversation

@qwen-code-dev-bot

@qwen-code-dev-bot qwen-code-dev-bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the ACP plan-mode authorization and JSON output formatting E2E coverage deterministic by serving fixed OpenAI-compatible responses locally. The plan-mode case now always requests a file write, verifies that ACP rejects it because plan mode is active, and verifies that no file is created. The JSON and streaming JSON cases continue to exercise the real CLI output pipeline without contacting the shared model service.

Each spawned ACP agent also receives an independent QWEN_HOME under the OS temporary directory. Cleanup retries tolerate late post-exit writes, and global setup reclaims abandoned test homes older than 24 hours.

Why it's needed

Main E2E run 34123176225 failed because the shared model service returned -32603 / model-serving errors. These cases verify local permission enforcement and output serialization rather than model quality, so a gateway outage should not fail them.

The retry also exposed an ENOTEMPTY teardown race: the ACP agent can keep writing to its home briefly after exit. Keeping that home outside the per-test workspace prevents late writes from racing workspace cleanup, while bounded best-effort cleanup avoids turning an otherwise green run red.

Reviewer Test Plan

How to verify

Confirm that the plan-mode scenario deterministically attempts a write, receives a failed tool update containing the plan-mode rejection, and leaves the target file absent. Confirm that JSON and streaming JSON output remain parseable, report success, and contain the fixed Paris response without contacting the shared model service. Confirm that ACP agents receive distinct temporary homes, cleanup failures warn without failing the run, and only abandoned homes older than 24 hours are swept.

Evidence (Before & After)

Before: run 34123176225 failed the ACP and JSON-output cases when the shared model service returned internal errors; the ACP retry could then fail teardown with ENOTEMPTY.

After: the focused ACP and JSON-output cases pass against the local fake service, and the scratch-home cleanup behavior is covered by focused tests. The current head passes the repository CI checks.

Tested on

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, no sandbox, local OpenAI-compatible fake server.

Risk & Scope

  • Main risk or tradeoff: these protocol cases no longer cover shared model-service availability; that remains separate from their assertions.
  • Not validated / out of scope: live-model health, ECS network egress, host-level runner capacity, and local Windows/Linux execution.
  • Cleanup boundary: only dedicated test-home prefixes older than 24 hours are swept; failed immediate cleanup is best-effort and may leave a home until the later sweep.
  • Breaking changes / migration notes: none; test-only change.

Linked Issues

References #11271

Fixes #11307

中文说明

这个 PR 做了什么

使用固定的本地 OpenAI 兼容响应,让 ACP plan mode 权限校验和 JSON 输出格式 E2E 测试变得确定。Plan mode 用例现在会必然请求写入文件,验证 ACP 因 plan mode 生效而拒绝该工具调用,并验证文件没有被创建。JSON 和流式 JSON 用例继续覆盖真实的 CLI 输出链路,但不再访问共享模型服务。

每个启动的 ACP agent 也会获得位于系统临时目录下的独立 QWEN_HOME。清理过程使用有限重试来容忍退出后的延迟写入,全局 setup 会回收超过 24 小时的遗留测试 home。

为什么需要

Main E2E run 34123176225 因共享模型服务返回 -32603 / model-serving 错误而失败。这些用例验证的是本地权限约束和输出序列化,而不是模型质量,因此不应因网关故障失败。

重试还暴露了一个 ENOTEMPTY 清理竞态:ACP agent 退出后仍可能短暂写入自己的 home。将该 home 放在单个测试 workspace 之外,可以避免延迟写入与 workspace 清理竞争;有限的 best-effort 清理也不会把原本绿色的测试变红。

Reviewer 测试计划

如何验证

确认 plan mode 场景会确定地尝试写入,收到包含 plan mode 拒绝信息的失败工具更新,并且目标文件不存在。确认 JSON 和流式 JSON 输出仍然可解析、报告成功并包含固定的 Paris 响应,同时不访问共享模型服务。确认 ACP agent 使用不同的临时 home,清理失败只告警而不会让测试失败,并且只有超过 24 小时的遗留 home 会被回收。

证据(修改前后)

修改前:run 34123176225 在共享模型服务返回内部错误时,ACP 和 JSON-output 用例失败;ACP 重试随后还可能因 ENOTEMPTY 导致清理失败。

修改后:定向 ACP 和 JSON-output 用例通过本地假服务运行,临时 home 的清理行为也有定向测试覆盖。当前 head 的仓库 CI 检查通过。

测试平台

系统 状态
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS,无 sandbox,本地 OpenAI 兼容假服务。

风险与范围

  • 主要风险或取舍:这些协议用例不再覆盖共享模型服务可用性;该能力与本用例断言分离。
  • 未验证 / 范围外:线上模型健康、ECS 网络出口、宿主机级 runner 容量,以及 Windows/Linux 本地执行。
  • 清理边界:只回收使用专用测试前缀且超过 24 小时的 home;即时清理失败时采用 best-effort,可能保留到后续 sweep。
  • 破坏性变更 / 迁移说明:无;仅测试改动。

关联 Issue

References #11271

Fixes #11307

The prompt-latency probe in qwen-serve-baseline sends 20 real prompts
through the shared OpenAI-compatible gateway and asserts p99 < 60s. One
gateway-queued prompt in twenty fails it, and each vitest retry re-issues
all 20 prompts into the same degraded window, so a single slow window
fails every attempt and turns the shard red.

Since the pool skip (#11004) the probe runs on exactly one E2E leg —
macOS shard 2/2, the only shard holding the file — and that leg failed
twice within six hours on unrelated commits (runs 34070970091 and
34088422718, both full-duration with retries consumed) while every Linux
leg, macOS shard 1/2, and the OpenTUI leg stayed green. The measured
quantity is gateway weather, not the daemon; the same argument the pool
skip made for host contention applies to the shared gateway.

Skip the probe whenever CI is set (any populated CI marker, matching the
repo's other CI checks), keeping the self-hosted disjunct so a
pool-shaped shell outside CI keeps its specific skip reason. The probe
still runs off CI on a credential, and
QWEN_BASELINE_ENABLE_PROMPT_LATENCY=1 force-runs it anywhere. The skip
reason distinguishes CI gateway contention from pool host contention.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

E2E Report — Issue #11271 (Main CI failed: E2E Tests on 36f2c14)

Summary

The tracked run (34088422718, commit 36f2c14f0e) failed two of eleven legs:

Leg Failed step Verdict
E2E Test - macOS - shard 2/2 Run E2E tests Prompt-latency probe measures shared-gateway weather — fixed by skipping it under CI
E2E Test (Linux) - sandbox:none - shard 3/3 Checkout Infrastructure failure before any repository code ran — no repo-side fix

Diagnosis

The commit cannot be the cause. 36f2c14f0e (feat(web-shell): align sidebar session scopes (#11240)) touches only packages/web-shell/client/** and docs — browser React code the CLI integration suite never loads. The same leg also failed six hours earlier on an unrelated core commit (62588d28, run 34070970091) and passed on the three commits in between, so this is an intermittent, leg-specific failure, not a regression from the tracked commit.

The failing leg narrows it to one test. Both failures were full-duration runs (9.1–9.4m vs 7.8–10.9m for greens), so a real test failed all three vitest attempts (retry: 2). Three independent facts single out the prompt-latency probe in integration-tests/cli/qwen-serve-baseline.test.ts:

  1. Shard membership: vitest list confirms the file sits in macOS shard 2/2 and not in shard 1/2; the OpenTUI leg runs only interactive/; so the file runs in exactly one workflow job — the only macOS leg that has ever failed. (The file's other tests run on the Linux legs too, and pass there.)
  2. Skip policy: since fix(test): stop measuring model latency on the shared E2E pool #11004 the probe skips on RUNNER_ENVIRONMENT=self-hosted (every Linux leg). The macOS leg never maps RUNNER_ENVIRONMENT, so with OPENAI_API_KEY present the probe runs there and only there.
  3. What it measures: 20 real prompts through the shared OpenAI-compatible gateway, asserting p99 < 60s. Local measurement on a healthy runner: p50 7.6s, max 16.5s — one gateway-queued prompt (~8× p50) crosses the threshold. Each vitest retry re-issues all 20 prompts into the same degraded window (60 prompts total), so one slow gateway window fails all attempts, and the probe's own runtime stretches the job to the observed full-duration failures. Failure times (00:48 and 05:51 UTC) are consistent with gateway load windows, and nothing changed in the probe or its inputs on Sep 7 — the weather simply turned.

Without access to the job log (GitHub returns 403 for unauthenticated log downloads) the failing test cannot be named from a log line; this is the evidence chain instead. The residual alternative in the same file — the 1s attach-latency bound — was weighed and rejected: the Linux pool legs run it under far worse host contention (~30 jobs per host, single fork) and pass.

The second leg is not actionable. sandbox:none shard 3/3 failed in Checkout — the job died before checkout completed, so no repository code participated. The workflow already carries a pre-checkout ownership heal; without logs there is no specific repo-side hypothesis, and none was implemented.

Fix

One commit: 180f5037bc fix(test): stop measuring model latency anywhere under CI (#11271)

  • integration-tests/cli/_prompt-latency-policy.ts — shouldSkipPromptLatency now also skips when CI is set (any populated value, matching the repo's other CI checks in osc8.ts/browser.ts), unless QWEN_BASELINE_ENABLE_PROMPT_LATENCY=1 force-runs it. The self-hosted disjunct stays so a pool-shaped shell outside CI keeps its specific skip reason, and promptLatencySkipReason reports CI gateway contention distinctly from pool host contention. Off CI the probe still runs on a credential, exactly as before.
  • integration-tests/cli/_prompt-latency-policy.test.ts — three new pinned behaviors: skip under CI with a credential, force-run override, pool-reason precedence under CI, and the CI reason text.

This extends the accepted rationale of #11004 ("N real model round-trips would measure host contention, not the daemon") to the variable the project controls even less: the shared gateway. Note the open PR #11134 covers a different failure class (all-green shard death) at the workflow level; the two are complementary, not overlapping.

Verification

  • npm run build — passed.
  • npm run typecheck — passed.
  • npm run lint — passed.
  • vitest run --root ./integration-tests cli/_prompt-latency-policy — 14/14 passed (re-run on the committed content after the pre-commit hook reformatted one line).
  • vitest run --root ./integration-tests cli/qwen-serve-baseline with CI=true (mirrors the macOS leg) — file passed, prompt-latency probe skipped via the recorded placeholder (8 tests | 1 skipped).
  • vitest run --root ./integration-tests cli/qwen-serve-baseline -t 'p50 / p99' without CI — probe still runs for real: 20 prompts against the live gateway, passed in 110s (p50 7.6s, max 16.5s).
  • Full local run of the exact macOS shard 2/2 command (37 files, pre-fix tree) — 36 files passed; only cli/qwen-config-dir.test.ts > 1d failed, on EACCES: mkdir /home/github-runner/.qwen, an artifact of this container's read-only $HOME (CI homes are writable) — nothing deterministic to fix.
  • Mutation probes (required witnesses): (a) removing the new CI disjunct → skips under CI even with a credential, unless force-run fails 3/3, restore → green; (b) removing the explicit pool branch in promptLatencySkipReason → both pool-reason tests fail, restore → 14/14 green.
  • An earlier hypothesis (a Ctrl+C exit race in skill-hooks-invocation-parity.test.ts) was tested with an injected 2s turn-tail delay and disproved — the exit hint renders even when the first Ctrl+C cancels a turn — so no change was made there.

Remaining risk

If macOS shard 2/2 reds continue after this lands, the failing test is something else in the 37-file shard and needs the job log to identify; the attach-latency bound in the same baseline file is the next candidate to inspect (argued against above, but not excludable without logs). The Checkout failure on the ECS pool is infrastructure-side and out of the repository's reach.

中文说明

E2E 报告 — Issue #11271(main 分支 E2E Tests 在 36f2c14 上失败)

摘要

被跟踪的运行(34088422718,提交 36f2c14f0e)在十一条腿中失败了两条:

腿 失败步骤 结论
E2E Test - macOS - shard 2/2 Run E2E tests prompt 延迟探针测量的是共享网关的"天气"——已在 CI 下跳过该探针修复
E2E Test (Linux) - sandbox:none - shard 3/3 Checkout 在任何仓库代码运行之前失败的基础设施问题——仓库侧无可修复项

诊断

该提交不可能是原因。 36f2c14f0e(feat(web-shell): align sidebar session scopes (#11240))只改动了 packages/web-shell/client/** 和文档——CLI 集成测试套件从不加载这些浏览器 React 代码。同一条腿在六小时前还在一个无关的 core 提交上失败过(62588d28,运行 34070970091),而期间的三个提交都通过了,因此这是间歇性的、特定于该腿的失败,而不是被跟踪提交引入的回归。

失败的腿把范围缩小到一个测试。 两次失败都是完整时长的运行(9.1–9.4 分钟,而绿色运行为 7.8–10.9 分钟),说明某个真实测试在 vitest 的三次尝试(retry: 2)中全部失败。三条相互独立的事实共同指向 integration-tests/cli/qwen-serve-baseline.test.ts 中的 prompt 延迟探针:

  1. 分片归属:vitest list 确认该文件只在 macOS shard 2/2 中,不在 shard 1/2 中;OpenTUI 腿只运行 interactive/ 目录;因此该文件只在唯一一个工作流作业中运行——恰好是唯一失败过的 macOS 腿。(该文件的其他测试也在 Linux 腿上运行,并且通过。)
  2. 跳过策略:自 fix(test): stop measuring model latency on the shared E2E pool #11004 起,探针在 RUNNER_ENVIRONMENT=self-hosted(所有 Linux 腿)上跳过。macOS 腿从不映射 RUNNER_ENVIRONMENT,所以在 OPENAI_API_KEY 存在时,探针只在那里运行。
  3. 它测量的内容:通过共享的 OpenAI 兼容网关发送 20 个真实 prompt,断言 p99 < 60s。本地在健康运行器上的测量:p50 为 7.6 秒,最大 16.5 秒——只要一个被网关排队的 prompt(约 8 倍 p50)就会越过阈值。每次 vitest 重试都会把全部 20 个 prompt 重新打进同一个劣化窗口(共 60 个),所以一次缓慢的网关窗口会让所有尝试失败,探针自身的运行时长也把作业拉长到观测到的完整时长失败。失败时间(UTC 00:48 和 05:51)与网关负载窗口一致,而探针及其输入在 9 月 7 日没有任何变化——只是"天气"变差了。

由于无法获取作业日志(GitHub 对未认证的日志下载返回 403),无法从日志行指认失败的测试;以上为证据链。同一文件中的剩余备选——1 秒的 attach 延迟上限——经过权衡后被排除:Linux 池腿在恶劣得多的主机竞争下(每台主机约 30 个作业、单 fork)运行它并且通过。

第二条腿不可操作。 sandbox:none shard 3/3 在 Checkout 步骤失败——作业在检出完成前就死了,没有任何仓库代码参与。工作流已经带有检出前的属主修复步骤;没有日志就没有具体的仓库侧假设,因此也未实施任何修改。

修复

一个提交:180f5037bc fix(test): stop measuring model latency anywhere under CI (#11271)

  • integration-tests/cli/_prompt-latency-policy.ts — shouldSkipPromptLatency 现在在 CI 被设置时(任何非空值,与仓库中 osc8.ts/browser.ts 的其他 CI 检查一致)也会跳过,除非 QWEN_BASELINE_ENABLE_PROMPT_LATENCY=1 强制运行。self-hosted 析取保留,以便 CI 之外的池形态环境仍得到其特定的跳过原因;promptLatencySkipReason 现在把 CI 网关竞争与池主机竞争区分开报告。在 CI 之外,探针仍凭凭证运行,与之前完全一致。
  • integration-tests/cli/_prompt-latency-policy.test.ts — 新增三个固定行为:CI 下有凭证时跳过、强制运行开关的覆盖、CI 下池原因的优先级,以及 CI 原因文案。

这把 #11004 已接受的论据("N 次真实模型往返测量的是主机竞争,而不是守护进程")推广到了项目更无法控制的那个变量:共享网关。注意处于打开状态的 PR #11134 覆盖的是另一种失败类别(全绿分片死亡),在工作流层面;两者互补而不重叠。

验证

  • npm run build — 通过。
  • npm run typecheck — 通过。
  • npm run lint — 通过。
  • vitest run --root ./integration-tests cli/_prompt-latency-policy — 14/14 通过(在 pre-commit 钩子重排一行之后,按提交内容重跑)。
  • CI=true 下 vitest run --root ./integration-tests cli/qwen-serve-baseline(模拟 macOS 腿)——文件通过,prompt 延迟探针按记录的占位测试跳过(8 tests | 1 skipped)。
  • 无 CI 下 vitest run --root ./integration-tests cli/qwen-serve-baseline -t 'p50 / p99' ——探针仍真实运行:对真实网关发送 20 个 prompt,110 秒通过(p50 7.6 秒,最大 16.5 秒)。
  • 用与 macOS shard 2/2 完全相同的命令做完整本地运行(37 个文件,修复前的树)——36 个文件通过;只有 cli/qwen-config-dir.test.ts > 1d 失败,原因是 EACCES: mkdir /home/github-runner/.qwen,这是本容器 $HOME 只读的产物(CI 的 home 可写)——没有确定性的东西可修。
  • 变异探针(必需的见证):(a) 删除新增的 CI 析取 → skips under CI even with a credential, unless force-run 三次尝试全失败,恢复后转绿;(b) 删除 promptLatencySkipReason 中显式的池分支 → 两个池原因测试失败,恢复后 14/14 全绿。
  • 一个较早的假设(skill-hooks-invocation-parity.test.ts 中的 Ctrl+C 退出竞态)通过注入 2 秒回合尾部延迟进行了检验并被证伪——即使第一次 Ctrl+C 取消的是进行中的回合,退出提示也会渲染——因此未在该文件做任何改动。

剩余风险

如果合入后 macOS shard 2/2 继续变红,失败的测试就是该 37 文件分片中的其他测试,需要作业日志来确认;同一基线文件中的 attach 延迟上限是下一个候选(上文已论证排除,但没有日志无法彻底排除)。ECS 池上的 Checkout 失败属于基础设施侧,超出仓库可达范围。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 7, 2026
@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

✅ AutoFix round 5 finished — view run. See this round's report below.

中文说明

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

…de case (#11271)

The CI-wide prompt-latency skip rested on a misattribution: in both
cited runs the latency probe ran and passed, and the macOS leg went
red on cli/acp-integration.test.ts > blocks write tools in plan mode —
killed by a transient model-serving -32603 returned over ACP, with the
retry attempt then tripping ENOTEMPTY on the previous attempt's
leftover .qwen-home. Revert the CI skip, re-issue the plan-mode
session/prompt on the observed gateway -32603 shape (bounded at 3
attempts, 2s apart, inside the existing per-request timeout), and let
the TestRig.setup directory reset retry through the mid-delete refill
race the way globalSetup's teardown already does.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

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

Autofix round — PR #11286 (issue #11271)

Outcome: the CI-wide prompt-latency skip is reverted, and the fix is retargeted at the test that actually failed twice — cli/acp-integration.test.ts > blocks write tools in plan mode (issue #1806).

The reviews established from the cited runs' own job logs that the PR's premise did not hold: in both runs (34070970091, 34088422718) the prompt-latency probe ran and passed, while the leg went red on the acp plan-mode case dying on a transient model-serving -32603 over ACP, and the retry attempt then tripped ENOTEMPTY on the previous attempt's leftover .qwen-home. This round implements exactly that retargeting.

Per-finding dispositions

R1-1 (rc:3948529249, Critical) — Resolved in code

  • Revert: integration-tests/cli/_prompt-latency-policy.ts and _prompt-latency-policy.test.ts are restored byte-exact to origin/main (verified: git diff origin/main -- <files> is empty). The Boolean(env['CI']) disjunct, the CI gateway-contention reason, and the three added test cases are gone. The probe keeps its only CI home — the macOS E2E legs.
  • Transient -32603 tolerance: new integration-tests/cli/_acp-model-serving-retry.ts exports isTransientModelServingError (matches only the observed shape: JSON-RPC -32603 whose serialized error data names model serving — an agent-raised -32603 such as loop protection fails fast on the first attempt) and withTransientModelServingRetry (bounded: 3 attempts, 2s apart). The plan-mode case's session/prompt (acp-integration.test.ts:1003) is wrapped in it. Budget: each attempt keeps its own REQUEST_TIMEOUT_MS = 60_000; worst case per test attempt is ~184s, inside the 5-minute test timeout, and vitest's retry: 2 semantics are unchanged. A persistent outage still fails the case (pinned by test).
  • ENOTEMPTY teardown race: TestRig.setup()'s directory reset (test-helper.ts:240) now passes maxRetries: 5, retryDelay: 200 — the same remedy globalSetup.ts's teardown already uses for the identical refill race, with a comment citing the observed failure. This is the rm the retry attempt re-enters while the previous attempt's CLI/daemon child is still draining writes under .qwen-home (CI sets KEEP_OUTPUT, so it is the only rm on that path).
  • Witness placement, transparently: the injection witness lives at the wrapper seam (_acp-model-serving-retry.test.ts) rather than as a full fake-server E2E, because the fake OpenAI server's 500 body is a fixed string (fake OpenAI server handler failed) that cannot carry the model-serving signature the predicate correctly requires — a fake-server injection cannot produce the observed -32603 shape through the real agent. The unit witness injects the verbatim observed response shape at exactly the seam where the defect lived (the waiter rejection propagating out of the plan-mode case's prompt), and both guards are pinned by mutation probes (below). The wrapped plan-mode case was also run end-to-end against the real model and passed (27.2s).

R1-2 (rc:3948529257) — Resolved in code

The comment recording the refuted causal claim (probe passed in both cited runs) is deleted by the revert; the restored origin/main comment makes no run attribution.

R1-3 (rc:3948529263) — Resolved in code

The unconditional CI fallthrough in promptLatencySkipReason is deleted by the revert; the restored function is aligned with the predicate again (pool is the only environment disjunct, and the reason says so), and the 11 pre-existing pinned cases pass.

R1-4 (rc:3948529277) — Resolved in code

Conditional on keeping a CI-wide skip; the skip is not kept. The measurement never leaves CI — the macOS legs keep executing and enforcing promptP99MaxMs — so no new home, follow-up issue, or p90 relaxation is needed.

R1-6 (rc:3948529282) — Resolved in code

The Boolean(env['CI']) predicate whose truthiness boundary needed pinning no longer exists after the revert.

Review rv:5129647277 — process note acknowledged

The note that failing job logs are retrievable via gh api repos/.../jobs/<job_id>/logs with actions:read is recorded; it concerns the bot's candidate-selection workflow, not this repository's code, and workflow/.qwen changes are out of bounds for an address round.

Deleted tests (gate advisory context)

The three deleted cases (skips under CI even with a credential, unless force-run; keeps the pool reason on a self-hosted CI runner; reports CI gateway contention off the pool) pin only the reverted CI disjunct/reason — behavior removed on CHANGES_REQUESTED review evidence (the cited CI logs). Retained behavior stays covered by the 11 surviving _prompt-latency-policy.test.ts cases, all green.

Conflict notes

No base-branch conflict (--conflict false); no merge performed.

Verification

  • npx vitest run test-helper.test.ts cli/_acp-model-serving-retry.test.ts pre-fix — teardown witness RED (rm called with {force, recursive} only, no maxRetries/retryDelay); retry suite green against the new module.
  • npx vitest run test-helper.test.ts cli/_acp-model-serving-retry.test.ts cli/_prompt-latency-policy.test.ts post-fix — 26 passed (8 + 7 + 11).
  • Mutation probe 1: MAX_ATTEMPTS = 1 (retry removed) → re-issues the prompt after a transient model-serving -32603 and still fails a persistent outage after a bounded number of attempts RED; restored, green.
  • Mutation probe 2: predicate matches any -32603 (model-serving signature removed) → does not match a -32603 raised by the agent itself and does not retry a -32603 the gateway did not cause RED; restored, green.
  • Mutation probe 3: maxRetries/retryDelay removed from TestRig.setup() → resets the reused directory with retries against the refill race RED; restored, green.
  • Pre-round simulation: _acp-model-serving-retry.test.ts against the pre-round tree (module absent) — RED (Cannot find module './_acp-model-serving-retry.js'), so the gate's pre-round failure requirement is met by this file and by the new test-helper.test.ts case.
  • npm run build — passed.
  • npm run typecheck (workspaces + typecheck:integration) — passed.
  • npm run lint (eslint . --ext .ts,.tsx && eslint integration-tests) — passed.
  • npx prettier --check on all changed files — passed (after one --write on the new test file).
  • npx vitest run cli/acp-integration.test.ts -t "blocks write tools in plan mode" against the real model with the wrapper in place — passed (27.2s, first-try success; retry transparent on the success path).
  • Revert byte-exactness: git diff origin/main -- integration-tests/cli/_prompt-latency-policy.ts integration-tests/cli/_prompt-latency-policy.test.ts — empty.
  • Not run: the full acp-integration.test.ts suite (10 unrelated cases, several minutes of real model calls; unchanged code paths), and a fake-server E2E injection (not faithfully constructible — see R1-1 witness note). CI remains the final gate.
中文说明

Autofix 本轮处理 — PR #11286(issue #11271)

结果:CI 全量跳过 prompt 延迟探针的改动已被回退,修复改投到真正失败了两次的测试上——cli/acp-integration.test.ts > blocks write tools in plan mode (issue #1806)。

两轮 review 依据所引用运行的一手作业日志证明:本 PR 的前提不成立。在这两次运行(34070970091、34088422718)中,prompt 延迟探针都真实运行并通过;把这条腿跑红的是 acp plan-mode 用例——它死于一次通过 ACP 返回的瞬时模型服务 -32603,随后的重试又在上一轮遗留的 .qwen-home 上撞出 ENOTEMPTY。本轮完全按这一重新定向实施。

逐项处理结论

R1-1(rc:3948529249,Critical)——已在代码中解决

  • 回退: integration-tests/cli/_prompt-latency-policy.ts 与 _prompt-latency-policy.test.ts 已逐字节恢复到 origin/main(已验证:git diff origin/main -- <files> 为空)。Boolean(env['CI']) 析取、CI 网关竞争原因文案以及新增的三个测试用例全部移除。探针保住了它在 CI 中唯一的归属——macOS E2E 腿。
  • 瞬时 -32603 容忍: 新增 integration-tests/cli/_acp-model-serving-retry.ts,导出 isTransientModelServingError(只匹配观测到的形态:序列化错误数据中含 model serving 字样的 JSON-RPC -32603——agent 自身抛出的 -32603(如 loop protection)仍在首次尝试即失败)与 withTransientModelServingRetry(有界:3 次尝试,间隔 2 秒)。plan-mode 用例的 session/prompt(acp-integration.test.ts:1003)已用它包裹。预算:每次尝试仍各自携带 REQUEST_TIMEOUT_MS = 60_000;单次测试尝试的最坏耗时约 184 秒,在 5 分钟测试超时之内,vitest retry: 2 语义不变。持续性故障仍然会让用例失败(有测试固定)。
  • ENOTEMPTY 清理竞争: TestRig.setup() 的目录重置(test-helper.ts:240)现在传入 maxRetries: 5, retryDelay: 200——与 globalSetup.ts teardown 对同一类 refill 竞争已有的处置完全一致,注释中注明了观测依据。CI 设置 KEEP_OUTPUT,因此这是该路径上唯一的 rm,而重试进入它时,上一轮尝试的 CLI/daemon 子进程仍在 .qwen-home 下收尾写入。
  • 见证测试的位置,如实说明: 注入式见证落在包装器接缝处(_acp-model-serving-retry.test.ts),而不是完整的 fake-server E2E,原因是 fake OpenAI server 的 500 响应体是固定字符串(fake OpenAI server handler failed),无法携带判定式所要求的 model serving 签名——经真实 agent 的 fake-server 注入无法产生观测到的 -32603 形态。单元见证把逐字观测到的响应形态注入到缺陷所在的确切接缝(waiter 的 rejection 传出 plan-mode 用例 prompt 的位置),且两个守卫都有变异探针固定(见下)。包裹后的 plan-mode 用例也用真实模型端到端跑过一次并通过(27.2 秒)。

R1-2(rc:3948529257)——已在代码中解决

记录被否证因果论断的注释(所引两次运行中探针均通过)随回退删除;恢复后的 origin/main 注释不含任何运行归因。

R1-3(rc:3948529263)——已在代码中解决

promptLatencySkipReason 中的无条件 CI 兜底随回退删除;恢复后的函数与判定式重新对齐(pool 是唯一的环境析取,原因文案如实表述),11 个既有固定用例全部通过。

R1-4(rc:3948529277)——已在代码中解决

该建议以保留 CI 全量跳过为前提;跳过没有保留。测量从未离开 CI——macOS 腿继续执行并强制 promptP99MaxMs——因此无需新的归属、后续 issue 或 p90 放宽。

R1-6(rc:3948529282)——已在代码中解决

需要固定真值边界的 Boolean(env['CI']) 判定式在回退后已不存在。

Review rv:5129647277 —— 流程提示已知悉

"失败作业日志可用持有 actions:read 的 token 通过 gh api repos/.../jobs/<job_id>/logs 获取"这一点已记录在案;它针对的是 bot 的候选选择流程,而非本仓库代码,且 workflow/.qwen 改动超出 address 轮次的边界。

删除的测试(供门禁 advisory 对照)

被删除的三个用例(skips under CI even with a credential, unless force-run、keeps the pool reason on a self-hosted CI runner、reports CI gateway contention off the pool)固定的只是被回退的 CI 析取/原因文案——该行为基于 CHANGES_REQUESTED review 证据(所引 CI 日志)被移除。保留行为的覆盖由 _prompt-latency-policy.test.ts 存活的 11 个用例承担,全部绿色。

冲突说明

无基线分支冲突(--conflict false);未做合并。

验证

  • 修复前 npx vitest run test-helper.test.ts cli/_acp-model-serving-retry.test.ts —— teardown 见证为红(rm 仅以 {force, recursive} 被调用,无 maxRetries/retryDelay);retry 套件对新模块为绿。
  • 修复后 npx vitest run test-helper.test.ts cli/_acp-model-serving-retry.test.ts cli/_prompt-latency-policy.test.ts —— 26 通过(8 + 7 + 11)。
  • 变异探针 1:MAX_ATTEMPTS = 1(移除重试)→ re-issues the prompt after a transient model-serving -32603 与 still fails a persistent outage after a bounded number of attempts 变红;恢复后转绿。
  • 变异探针 2:判定式匹配任意 -32603(移除 model serving 签名)→ does not match a -32603 raised by the agent itself 与 does not retry a -32603 the gateway did not cause 变红;恢复后转绿。
  • 变异探针 3:从 TestRig.setup() 移除 maxRetries/retryDelay → resets the reused directory with retries against the refill race 变红;恢复后转绿。
  • 轮前模拟:在轮前树上运行 _acp-model-serving-retry.test.ts(模块不存在)—— 红(Cannot find module './_acp-model-serving-retry.js'),因此门禁的"轮前失败"要求由该文件与 test-helper.test.ts 新用例共同满足。
  • npm run build —— 通过。
  • npm run typecheck(workspaces + typecheck:integration)—— 通过。
  • npm run lint(eslint . --ext .ts,.tsx && eslint integration-tests)—— 通过。
  • 对所有改动文件执行 npx prettier --check —— 通过(新测试文件先经一次 --write)。
  • 在包裹就位的情况下,用真实模型运行 npx vitest run cli/acp-integration.test.ts -t "blocks write tools in plan mode" —— 通过(27.2 秒,首次尝试即成功;重试在成功路径上透明)。
  • 回退逐字节核对:git diff origin/main -- integration-tests/cli/_prompt-latency-policy.ts integration-tests/cli/_prompt-latency-policy.test.ts —— 为空。
  • 未运行:完整 acp-integration.test.ts 套件(10 个无关用例,数分钟真实模型调用,代码路径未变),以及 fake-server E2E 注入(无法忠实构造——见 R1-1 见证说明)。CI 仍是最终门禁。

🦷 Gate advisory — this round resolves a Critical/Request-changes finding with test-only changes (machine-measured): the bite check cannot verify a test-side fix, so the resolution rests on the round summary alone. · 本轮以纯测试改动解决 Critical/Request-changes 反馈(门自动测量):bite 检查无法验证测试侧修复,该解决仅以轮次摘要为凭。

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

🧵 Resolved all 5 selected review thread(s). · 已关闭全部选中的 5 条评审线程。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@yiliang114 yiliang114 changed the title fix(test): stop measuring model latency anywhere under CI (#11271) fix(test): stabilize ACP plan-mode E2E after transient model errors (#11271) Sep 7, 2026
@yiliang114 yiliang114 changed the title fix(test): stabilize ACP plan-mode E2E after transient model errors (#11271) test(e2e): isolate protocol checks from model service Sep 7, 2026
chiga0
chiga0 previously approved these changes Sep 7, 2026

@chiga0 chiga0 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 findings.
Approval blockers: none.

Tier: Standard — E2E test isolation fix; two test files modified.

Checked:

  • Import contract (cross-file): fakeToolCall, FakeOpenAIServer, startFakeOpenAIServer exist and are exported from fake-openai-server.ts (lines 101, 75, 116). fakeServerHostOptions, applyContainerSandboxNoProxy exist in test-helper.ts (lines 149, 157). No missing-export risk.
  • ACP wire shape _meta.toolName: Confirmed by bridgeClient.ts:510 comment ("ToolCallEmitter stamps _meta.toolName") and tool-call-emitter.ts:180. Old test read top-level toolUpdate.toolName which was never populated.
  • status: 'failed': Confirmed in transcript-replay.ts:362 (options.success ? 'completed' : 'failed'). Old test checked 'error', which is not a valid enum value — the old assertion was always-false when write_file was blocked.
  • content[].content.text shape: Confirmed in buildToolResultContent (transcript-replay.ts:1502): errorMessage becomes { type: 'content', content: { type: 'text', text: errorMessage } }. emitProgressUpdate uses the same format. Test's map(({ content }) => content?.text) correctly extracts the error text.
  • Unconditional assertion: Old test guarded by if (writeFileEvents.length > 0) — always passed when LLM never called write_file. New test injects write_file deterministically via fake server, making the assertion unconditional and non-vacuous.
  • Timing: await sendRequest('session/prompt') resolves after full agent turn; all session/update notifications already delivered on same stdout stream. Removing await delay(2000) is correct.
  • Fake server lifecycle: fakeServer.close() in finally block (acp test) and afterEach (json-output test). Container sandbox proxy env properly restored.
  • 4th json-output test (auth mismatch): correctly omits fakeModelArgs() — fails before reaching model service.
  • Previous reviews R1-1/R2-x: All apply to intermediate commits (sha 180f503, a25ab8c) that touched _prompt-latency-policy.ts and _acp-model-serving-retry.ts. At current head (aa79c7c), net PR diff is two files only; none of the previously criticized changes are present. All prior findings are stale.

Not covered: No production code changed; Linux/Windows execution not validated by author but changes are environment-agnostic (fake server on localhost, no platform-specific paths).

Reviewed with AI assistance.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🕐 Review received — an automatic review of the current head is still running, so this round is held until it lands (a push now would cancel it and discard its work, #8888). Your feedback stays queued for the next eligible round.

中文说明

🕐 已收到评审 —— 当前 head 上仍有一轮自动 review 在运行,本轮暂缓(现在推送会取消该 review 并丢弃其工作,#8888)。反馈保持排队,等待下一次可运行的轮次处理。

@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.

Review at head aa79c7c1e3 (merge-base a1cbe75cf1). No blocking findings. 3 inline: 1 P2, 2 P3.

The central change checks out, and it is stronger than the description claims. I traced the rewritten plan-mode assertion into production rather than trusting the diff:

  • isPlanModeBlocked → earlyErrorResponse (packages/cli/src/acp-integration/session/Session.ts:12525-12536) carrying the literal Plan mode is active. The tool "..." cannot be executed (:12527) → ToolCallEmitter.emitError (session/emitters/tool-call-emitter.ts:258-282) → createTranscriptToolCallResultUpdate (packages/acp-bridge/src/transcript-replay.ts:354-381). That builder sets status: 'failed' when success is false (:362), places toolName in _meta through buildUpdateMeta's top-level ...options.extra spread (:239 with :368), and carries the message as content[0].content.text through buildToolResultContent's errorMessage branch (:1502-1509). Every field the new collector reads matches production exactly.
  • The same read shows the update object has no top-level toolName, which means the pre-diff test was vacuous rather than merely weak: toolUpdate.toolName was always undefined, toolCallEvents stayed empty, and the removed if (writeFileEvents.length > 0) guard meant not one assertion in that block ever executed. Reading _meta.toolName and dropping the guard turns a dead test into a live one. Worth stating plainly, because it is the strongest argument for this PR and the description frames it only as "a weak assertion".
  • The strengthened form is non-tautological in both directions: with permissionHandler returning proceed_once for non-switch_mode requests (:1003), broken enforcement yields no 'failed' write_file event and creates the file, so :1066 and :1074 both go red; a tool call that never arrives also goes red at :1066 instead of passing silently as before.
  • Dropping await delay(2000) is safe, not a new race: sessionUpdates is appended synchronously in the single readline handler and JSON-RPC notifications precede the session/prompt response on the same stdout stream, so the blocking update is already collected when sendRequest resolves.
  • setupAcpTest merges the injected env as { ...process.env, ...options?.env, QWEN_HOME: qwenHome } (:136), so per-agent QWEN_HOME isolation cannot be clobbered. join is imported (:9); rig.testDir is assigned in TestRig.setup before the handler closure evaluates; and the trailing absence check at :1072 uses the same path the fake tool call requests at :974.
  • json-output.test.ts is fully isolated, not partially: exactly four tests, three take ...fakeModelArgs() (:58, :93, :169), and the fourth (:269) fails pre-flight on enforced auth mismatch before any model request.

Verification gap, disclosed. I executed nothing: this head has no node_modules locally and the machine's root filesystem is at 100% (765 MB free), so no install, build, or test run was possible. CI at this head is 14 SUCCESS / 0 failed / 25 SKIPPED with only review-pr in progress — but per the P2 inline, neither changed file runs on any pull_request lane, so green CI is not evidence these assertions pass. The only execution evidence is the author-reported local macOS run in the PR body.

Stale review state, for whoever lands this. reviewDecision reads CHANGES_REQUESTED from two round-1 bot reviews submitted at head 180f5037bc. Their Critical (ledger R1-1, _prompt-latency-policy.ts:51) targeted the earlier CI-wide latency-skip approach, which the rework abandoned: that file is byte-identical to base in this diff, so the finding is superseded rather than fixed. Those reviews need dismissing, or a fresh bot pass at this head, for the state to clear — chiga0 already approved at aa79c7c1e3.

Comment thread integration-tests/cli/json-output.test.ts
Comment thread integration-tests/cli/acp-integration.test.ts
Comment thread integration-tests/cli/json-output.test.ts Outdated
…#11271)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

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

Autofix round — PR #11286 (issue #11271)

Context this round

The six round-2 bot findings (R2-1, R2-2, R2-3, R2-4, R2-6, R2-7) were posted against the retry-based approach of a25ab8c75c. Maintainer commit e59cba50c6 ("test(e2e): isolate protocol checks from model service") then replaced that approach: _acp-model-serving-retry.ts and its test were deleted, the test-helper.ts rm retry options and their witness were reverted, and the plan-mode case now runs against an injected fake OpenAI server. Verified at this head: git diff origin/main...HEAD touches only integration-tests/cli/acp-integration.test.ts and integration-tests/cli/json-output.test.ts; _acp-model-serving-retry* does not exist; test-helper.ts is byte-identical to base.

Findings

Resolved in code

  • [rc:3950421657] R2-1 — test-helper.test.ts rm-retry witness (Suggestion): superseded. The maxRetries/retryDelay options this finding asked to pin were removed from test-helper.ts by e59cba50c6; the file is byte-identical to base and no longer appears in the PR diff, so there is nothing left to witness. The teardown race those options absorbed is also no longer exercised, because the case no longer talks to the real gateway.
  • [rc:3950421682] R2-2 — _acp-model-serving-retry.test.ts missing from the no-AK allowlist (Suggestion): superseded. The file was deleted by e59cba50c6; no allowlist entry is needed.
  • [rc:3950421689] R2-3 — classifier keys on one English keyword (Suggestion): superseded. isTransientModelServingError and its module were deleted by e59cba50c6.
  • [rc:3950421693] R2-4 — nothing pins that the prompt routes through the wrapper (Suggestion): superseded. The wrapper was removed by e59cba50c6. The replacement is self-witnessing: the case can only observe a write_file tool call if the injected OPENAI_BASE_URL really routes to the fake server, and with the old vacuous guard gone a missing tool call fails blockedEvent loudly.
  • [rc:3950421705] R2-7 — absorbed retry leaves no trace (Suggestion): superseded. The retry helper was deleted by e59cba50c6; there is no absorbed retry left to log.
  • [rc:3951510489] json-output.test.ts — unconditional close() on a maybe-unbound fakeServer (P3): implemented. afterEach now runs await fakeServer?.close() (integration-tests/cli/json-output.test.ts:37), matching the established idiom in submitted-prompt-provenance.test.ts:42, qwen-serve-streaming.test.ts:416, qwen-serve-client-mcp.test.ts:197, gen-ai-telemetry.test.ts:75, and skill-hooks-invocation-parity.test.ts:79. A startFakeOpenAIServer rejection in beforeEach now surfaces the original bind failure instead of a masking TypeError. restoreNoProxy() needs no guard: it is assigned synchronously before the server start, and applyContainerSandboxNoProxy() only reads and writes process.env.

Not resolved (replied on the thread, left open)

  • [rc:3950421700] R2-6 — PR title/description still describe the round-1 change (Suggestion): escalated. Still accurate at this head, and this lane has no GitHub credentials to edit PR metadata. The thread names a concrete replacement subject — test(e2e): isolate protocol checks from model service (#11271) — for a maintainer or the owning workflow to apply. Because this repository is squash-merge-only, the stale title would otherwise land in main's history.
  • [rc:3951510478] json-output.test.ts — add the file to the no-AK allowlist (P2): deferred to the follow-up queue. Verified: at this head all four cases are credential-free, and neither changed file is among the 23 entries of the test:integration:no-ak:sandbox:none allowlist (package.json:60), so no pull_request lane executes them. Not implemented here because the fix edits the root package.json scripts field — CI/verification machinery this PR has never touched and this lane is forbidden to modify — and the byte-pinned mirror in scripts/tests/no-ak-integration-ci.test.js must change in the same commit. Recorded in deferred-findings.json.
  • [rc:3951510484] acp-integration.test.ts — positional coupling of the fake-server tool call (P3, optional): declined. The failure this guards against is loud, not silent (with the vacuous if (writeFileEvents.length > 0) guard removed, a misdelivered tool call fails blockedEvent red, as the finding itself notes), there is no current producer of a pre-turn streaming request, and the stream counter is the dominant fake-server idiom in this repository (~30 uses in sdk-typescript/tool-control.test.ts alone, plus acp-cron.test.ts, gen-ai-telemetry.test.ts, and the interactive suites). Keying off prompt text would add message-shape assumptions (string vs structured content parts, plus role bookkeeping so the post-block follow-up request does not re-receive the tool call) to cover a hypothetical — exactly what AGENTS.md's Simplicity First rules out. Recorded on the thread so the deferral is visible.

Non-actionable items

  • [ic:5574275470] / failed check review-pr: the review budget timed out on the bot side; no code change can address that. A fresh review pass at this head should also clear the stale CHANGES_REQUESTED state left by the round-1 bot reviews, which the maintainer noted needs dismissing.

Verification

  • npm run build — passed
  • npm run typecheck — passed (includes tsc -p integration-tests/tsconfig.json)
  • npm run lint — passed
  • npx vitest run --root ./integration-tests --retry=0 cli/json-output.test.ts — 4 passed (the file changed this round)
  • npx vitest run --root ./integration-tests --retry=0 cli/acp-integration.test.ts -t 'blocks write tools in plan mode' — 1 passed, 10 skipped (the PR's central case, unchanged this round, verified green at this head)
  • Mutation probe on this round's change: temporarily reverted fakeServer?.close() to fakeServer.close() and reran cli/json-output.test.ts — 4 passed (the mutant survives, as expected: the unbound-server teardown path is unreachable in a green suite, and a port-0 bind failure cannot be induced deterministically, so no witness test is feasible; kept as a one-character diagnostics fix per the maintainer's request and the sibling idiom). Restored the guard and reran — 4 passed.
中文说明

Autofix 本轮处理 — PR #11286(issue #11271)

本轮背景

第 2 轮机器人审查的六条发现(R2-1、R2-2、R2-3、R2-4、R2-6、R2-7)针对的是 a25ab8c75c 中基于重试的方案。随后维护者提交 e59cba50c6("test(e2e): isolate protocol checks from model service")替换了该方案:_acp-model-serving-retry.ts 及其测试被删除,test-helper.ts 的 rm 重试选项及其见证测试被回退,plan-mode 用例现在改为对注入的 fake OpenAI 服务器运行。已在当前 head 验证:git diff origin/main...HEAD 只触及 integration-tests/cli/acp-integration.test.ts 与 integration-tests/cli/json-output.test.ts;_acp-model-serving-retry* 不存在;test-helper.ts 与基分支逐字节一致。

发现逐条处理

已在代码中解决

  • [rc:3950421657] R2-1 — test-helper.test.ts 中 rm 重试的见证(Suggestion):已被取代。 该发现要求固定的 maxRetries/retryDelay 选项已被 e59cba50c6 从 test-helper.ts 中移除;该文件与基分支逐字节一致,不再出现在 PR diff 中,因此没有需要固定的东西。这些选项所吸收的清理竞争也不再被触发,因为该用例不再访问真实网关。
  • [rc:3950421682] R2-2 — _acp-model-serving-retry.test.ts 未列入 no-AK 白名单(Suggestion):已被取代。 该文件已被 e59cba50c6 删除,无需再添加白名单条目。
  • [rc:3950421689] R2-3 — 判定式只依赖一个英文关键词(Suggestion):已被取代。 isTransientModelServingError 及其模块已被 e59cba50c6 删除。
  • [rc:3950421693] R2-4 — 没有测试固定 prompt 经过包装器(Suggestion):已被取代。 包装器已被 e59cba50c6 移除。替代方案是自我见证的:只有当注入的 OPENAI_BASE_URL 真的路由到 fake 服务器时,用例才能观测到 write_file 工具调用;而且旧的形同虚设的条件守卫已删除,工具调用缺失会让 blockedEvent 断言响亮地失败。
  • [rc:3950421705] R2-7 — 被吸收的重试不留痕迹(Suggestion):已被取代。 重试 helper 已被 e59cba50c6 删除,不存在需要记录日志的被吸收重试。
  • [rc:3951510489] json-output.test.ts — 对可能未赋值的 fakeServer 无条件调用 close()(P3):已实现。 afterEach 现在执行 await fakeServer?.close()(integration-tests/cli/json-output.test.ts:37),与 submitted-prompt-provenance.test.ts:42、qwen-serve-streaming.test.ts:416、qwen-serve-client-mcp.test.ts:197、gen-ai-telemetry.test.ts:75、skill-hooks-invocation-parity.test.ts:79 中的既有惯例一致。当 beforeEach 中 startFakeOpenAIServer 失败时,现在会呈现原始的绑定失败,而不是一个掩盖真实原因的 TypeError。restoreNoProxy() 不需要守卫:它在服务器启动之前同步赋值,且 applyContainerSandboxNoProxy() 只读写 process.env。

未解决(已在对应讨论串回复,保持开启)

  • [rc:3950421700] R2-6 — PR 标题/描述仍在描述第 1 轮的改动(Suggestion):升级给维护者。 在当前 head 仍然准确,而本执行通道没有 GitHub 凭证,无法编辑 PR 元数据。讨论串中给出了具体的替代标题——test(e2e): isolate protocol checks from model service (#11271)——供维护者或所属工作流采用。由于本仓库仅允许 squash 合并,否则这个过时的标题会写入 main 的历史。
  • [rc:3951510478] json-output.test.ts — 将该文件加入 no-AK 白名单(P2):推迟到后续队列。 已验证:当前 head 上全部四个用例都不依赖凭证,而两个被改文件都不在 test:integration:no-ak:sandbox:none 白名单(package.json:60)的 23 个条目中,因此没有任何 pull_request 通道执行它们。本轮未实现,因为该修复要修改根 package.json 的 scripts 字段——这是本 PR 从未触及、且本通道被禁止修改的 CI/验证机制——并且 scripts/tests/no-ak-integration-ci.test.js 中逐字节固定的镜像必须在同一提交中一起改。已记录到 deferred-findings.json。
  • [rc:3951510484] acp-integration.test.ts — fake 服务器工具调用的位置耦合(P3,可选):婉拒。 该建议防范的失败是响亮的而非静默的(正如发现本身所述,形同虚设的 if (writeFileEvents.length > 0) 守卫删除后,工具调用错投会让 blockedEvent 断言变红),目前不存在任何在 user turn 之前发出流式请求的产生方,而且流式计数器是本仓库 fake 服务器的主流惯例(仅 sdk-typescript/tool-control.test.ts 就有约 30 处,此外还有 acp-cron.test.ts、gen-ai-telemetry.test.ts 和各 interactive 套件)。按 prompt 文本做键控则会引入消息形态假设(字符串与结构化 content parts 之别,以及在工具被阻止后的后续请求中避免再次下发工具调用所需的角色簿记),去覆盖一个假想场景——这正是 AGENTS.md 的 Simplicity First 所排除的。已在讨论串中记录,确保这次婉拒可见。

无法行动的事项

  • [ic:5574275470] / 失败检查 review-pr: 机器人侧审查预算超时;没有任何代码改动能解决它。在当前 head 进行一次新的审查,还应能清除第 1 轮机器人审查留下的过时 CHANGES_REQUESTED 状态——维护者已指出该状态需要被撤销。

验证

  • npm run build — 通过
  • npm run typecheck — 通过(包含 tsc -p integration-tests/tsconfig.json)
  • npm run lint — 通过
  • npx vitest run --root ./integration-tests --retry=0 cli/json-output.test.ts — 4 通过(本轮修改的文件)
  • npx vitest run --root ./integration-tests --retry=0 cli/acp-integration.test.ts -t 'blocks write tools in plan mode' — 1 通过,10 跳过(本 PR 的核心用例,本轮未改,在当前 head 验证为绿)
  • 针对本轮改动的变异探测:临时把 fakeServer?.close() 还原为 fakeServer.close() 并重跑 cli/json-output.test.ts — 4 通过(变异体存活,符合预期:未绑定服务器的清理路径在绿色套件中不可达,而端口 0 的绑定失败无法确定性地诱发,因此无法编写可行的见证测试;按维护者要求及兄弟文件惯例,保留这一字符的诊断性修复)。恢复守卫后重跑 — 4 通过。

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

🧵 Resolved all 6 selected review thread(s). · 已关闭全部选中的 6 条评审线程。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — PR #11286

Verified as maintainer against head 3af26895e7, on a standalone worktree with the real bundled CLI (npm run build && npm run bundle → dist/cli.js), a real qwen --acp agent process, real Docker, and a recording stand-in for the shared model gateway. No mocks of the product under test; every mutation was applied to product source and re-bundled (bundle chunk hash checked each time, and confirmed to return byte-for-byte to acpAgent-XBVLQOC3.js when restored).

Verdict: the PR's claims hold, and the case for it is stronger than the description says. One PR-introduced issue found (medium, low CI impact but it makes the reviewer's own check flaky), plus one pre-existing gap the PR could close in the same pass.


1. The assertion this PR replaces could never have fired

The description calls main's assertion "weak … allowed the plan-mode case to pass when the model never attempted the write." It is stronger than that: on main the block was structurally unreachable, for every possible model behaviour.

I captured the real session/update frames from a live ACP agent and ran both extractors over that identical payload:

Fig 1

Three independent field mismatches — the tool name is at _meta.toolName (not top level), the status is "failed" (not "error"), and the reason is in content[0].content.text (not error.message). So writeFileEvents was always empty, if (writeFileEvents.length > 0) never ran, and the only surviving check was expect(fileExists).toBe(false) — which also passes when nothing is attempted.

2. The new assertion is load-bearing; main's is not

I mutated product source and re-bundled. Both arms use the PR's deterministic driver, so the model always attempts the write — the most favourable possible conditions for main's assertion.

Fig 2

# Mutation main's assertions PR
M1 plan-mode blocking removed (Session.ts:12515) FAIL (via fileExists only) FAIL (names the defect)
M2 block enforced, reason degraded (Session.ts:12527) PASS — blind FAIL — caught
M3 block enforced, reported to the client as completed (transcript-replay.ts:362) PASS — blind FAIL — caught

M2 and M3 are what an ACP client actually feels: the write is refused, but the editor can no longer say why, or is told the call succeeded.

And the decisive case — ship a build where plan mode blocks nothing (M1), with a healthy model that answers with a plan instead of calling write_file (the ordinary outcome):

Fig 3

main's blocks write tools in plan mode (issue #1806) reports green on a build where plan mode does not block writes at all. The PR's version goes red and names the defect.

3. The CI outage reproduces, and the isolation is measured — not assumed

A sentinel stands in for the shared gateway, answers every request 500 {"type":"internal_error","code":"model_serving_error"}, and records every request it receives. OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL point at it in both arms, as the CI secrets do.

Fig 4

Suite Arm Result Requests to the shared gateway Wall clock
ACP plan mode main FAIL — Request 5 (session/prompt) timed out 24 60 s budget exhausted
ACP plan mode PR PASS 0 1.3 s
json-output (4) main 3 failed, 1 passed 84 92.2 / 97.3 / 95.4 s
json-output (4) PR 4 passed 0 1.40 / 1.33 / 1.33 / 1.05 s

Repeatability with the gateway configured but down — ACP plan mode 15/15, json-output 10/10, 0 gateway requests across all 25 runs.

4. The Linux/container half (marked ⚠️ in the PR) works

I could not build the repo's own sandbox image locally (this Docker lacks buildx), so I exercised the same reachability path directly: this build's dist/cli.js running inside a Linux container, dialling the host-side fake server through host.docker.internal with the PR's exact flags.

Fig 6

Every field the three JSON cases assert on is produced: type:"result", is_error:false, result:"Paris", stats. Host ledger shows the two expected requests (main turn + managed-auto-memory-extractor).

Also clean on the PR arm: tsc -p integration-tests/tsconfig.json, eslint, prettier --check.


Findings

F1 — medium — dropping await delay(2000) opens a ~300 ms teardown race

Fig 5

Every assertion passes and the run still exits non-zero:

 Test Files  1 passed (1)
      Tests  1 passed | 10 skipped (11)

⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
[Error: ENOTEMPTY: directory not empty, rmdir '.../acp-plan-mode-enforcement/.qwen-home']

Attribution — the arms differ only in the settle window:

Arm Runs Assertions ENOTEMPTY at teardown
PR as submitted 45 45/45 pass 11 (24%)
PR + await delay(2000) restored (only difference) 20 20/20 pass 0
main's assertions + PR driver + delay(2000) 20 20/20 pass 0
whole acp-integration.test.ts file, PR arm 6 11/11 ×6 0

Mechanism, measured from cleanup() returning: +283 ms projects/<slug>/meta.json, +274 ms projects/<slug>/memory/MEMORY.md, +290 ms usage_record.jsonl — then nothing for the next 10 s. setupAcpTest puts the agent's QWEN_HOME inside rig.testDir; cleanup() awaits the agent's exit, but ~300 ms of writes still land there afterwards. On main the 2 s wait — plus a multi-second real-model turn — always covered that window. The PR's turn now finishes in 1.3 s with no wait, so global teardown's recursive rm can start inside it. retry: 2 does not retry teardown errors.

Impact. Low for CI as configured: e2e.yml shards whole files and two more tests follow this one, so the window closes — 0/6 in full-file runs. Real for a reviewer: the isolated -t run, which is the natural way to check this PR, is red about one time in four on macOS.

Suggestion — please don't just put the sleep back. Either create the agent's QWEN_HOME outside rig.testDir (mkdtempSync(join(tmpdir(), 'acp-home-')), cleaned in cleanup()), or have cleanup() wait for the directory to stop changing. Both keep the determinism this PR is buying; a blind 2 s sleep re-adds the thing the PR set out to remove.

F2 — low, pre-existing — case 4 of json-output.test.ts is still coupled to the CI secrets

should return a JSON error for enforced auth mismatch before running never calls the model, but it needs all three of OPENAI_API_KEY + OPENAI_BASE_URL + OPENAI_MODEL to exist so the CLI resolves the current auth type as openai. With any one missing it fails on both arms:

AssertionError: expected 'No auth type is selected. Please conf…' to contain 'configured auth type is qwen-oauth'

Not a regression — but it is the one case in this file the PR's stated goal does not reach, and it closes with the same ...fakeModelArgs() the other three now use (or by setting the three vars explicitly). Worth doing here so the file becomes hermetic as a whole.

F3 / F4 — nits

  • F3. The non-streaming branch ({"selected_memories":[]}) is never exercised in either file. Instrumented ledgers: the ACP case sees exactly 2 requests, both streaming (#0 stream=true msgs=2 tools=30, #1 stream=true msgs=4); json-output.test.ts sees 6, all streaming. Harmless as a safety net — just don't read it as covering a live path.
  • F4. streamingRequestIndex++ === 0 encodes "the first streaming request is the user's turn". True today (measured above). If a streaming side query is ever issued ahead of the turn, the scripted write_file lands on the wrong request and the failure will be confusing. A predicate on the request body (e.g. the last message being the user prompt) would survive that.

Recommendation

Approve after F1. The core of this PR is right and the evidence is stronger than the description claims — it does not merely stabilise a flaky test, it replaces one that could not fail. F1 is the only thing I would hold on, and it is a few lines in setupAcpTest. F2 is a natural companion fix; F3/F4 are optional.

Reproduction
# tree
git worktree add --detach wt11286 3af26895e7 && cd wt11286
npm install && npm run build && npm run bundle

# headline: PR passes with the shared gateway configured but down, and never calls it
OPENAI_API_KEY=k OPENAI_BASE_URL=http://127.0.0.1:<dead>/v1 OPENAI_MODEL=m \
QWEN_HOME=$(mktemp -d) QWEN_SANDBOX=false \
  npx vitest run --root ./integration-tests cli/json-output.test.ts --retry=0
QWEN_HOME=$(mktemp -d) QWEN_SANDBOX=false \
  npx vitest run --root ./integration-tests cli/acp-integration.test.ts --retry=0 \
    -t 'blocks write tools in plan mode'

# F1: repeat the isolated ACP run ~20x with a fresh QWEN_HOME each time and
#     grep the output for ENOTEMPTY — ~1 in 4 exits non-zero with every test green.

# mutations (each: edit source, `npm run bundle`, re-run)
#   M1 Session.ts:12515      prefix the plan-mode guard with `false &&`
#   M2 Session.ts:12527      replace the "Plan mode is active…" message
#   M3 transcript-replay.ts:362  status: options.success ? 'completed' : 'failed'  ->  'completed'
中文说明

PR #11286 本地验证报告

以维护者身份针对 head 3af26895e7 验证。使用独立 worktree、真实打包 CLI(npm run build && npm run bundle → dist/cli.js)、真实 qwen --acp 进程、真实 Docker,以及一个会记录全部请求的共享模型网关替身。被测产品不打桩;每个变异都改产品源码并重新 bundle(每次核对 bundle chunk 哈希,还原后逐字节回到 acpAgent-XBVLQOC3.js)。

结论:PR 的论点成立,而且实际理由比描述里写的更强。 发现 1 个本 PR 引入的问题(中等;对 CI 影响低,但会让评审者自己的验证命令变得不稳定),另有 1 个既有缺口建议在本次一并收口。

1. 被替换掉的那条断言根本不可能触发

描述说 main 的断言"过弱……模型没尝试写入时也能通过"。实际更严重:在 main 上该断言块结构性不可达,对任何模型行为都是如此。

我从真实 ACP agent 抓下 session/update 帧,用同一份 payload 分别跑两个提取器(图 1):工具名在 _meta.toolName(不在顶层),状态是 "failed"(不是 "error"),原因在 content[0].content.text(不在 error.message)。三处字段全不匹配,所以 writeFileEvents 恒为空,if (writeFileEvents.length > 0) 永不执行,唯一还起作用的只剩 expect(fileExists).toBe(false) —— 而模型什么都没做时它同样通过。

2. 新断言是承重件,main 的不是

改产品源码 + 重新 bundle。两臂都用 PR 的确定性驱动,即模型必然尝试写入 —— 这已经是对 main 断言最有利的条件(图 2):

# 变异 main 的断言 PR
M1 删除 plan mode 拦截(Session.ts:12515) FAIL(仅靠 fileExists) FAIL(直接指出缺陷)
M2 仍然拦截,但拒绝原因被降级(Session.ts:12527) PASS —— 看不见 FAIL —— 抓到
M3 仍然拦截,但向客户端报告为 completed(transcript-replay.ts:362) PASS —— 看不见 FAIL —— 抓到

M2、M3 正是 ACP 客户端真正会感知到的回归:写入被拒了,但编辑器再也说不出为什么,或者被告知调用成功了。

决定性的一组(图 3):产品端 plan mode 完全不拦截(M1),模型像平常那样只回一份计划、不调用 write_file —— main 的 blocks write tools in plan mode (issue #1806) 报绿。PR 版本报红并指出缺陷。

3. CI 故障可复现,隔离性是量出来的,不是假设的

替身网关对所有请求返回 500 {"type":"internal_error","code":"model_serving_error"},并记录每一个到达的请求。两臂的 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL 都指向它,和 CI secrets 一样(图 4):

套件 臂 结果 打到共享网关的请求数 耗时
ACP plan mode main FAIL —— Request 5 (session/prompt) timed out 24 耗尽 60 s 预算
ACP plan mode PR PASS 0 1.3 s
json-output(4 例) main 3 失败 1 通过 84 92.2 / 97.3 / 95.4 s
json-output(4 例) PR 4 通过 0 1.40 / 1.33 / 1.33 / 1.05 s

网关配置存在但宕机时的重复性:ACP plan mode 15/15,json-output 10/10,25 次运行累计 0 次网关请求。

4. PR 标 ⚠️ 的 Linux/容器那一半可用

本机 Docker 缺 buildx,仓库自带 sandbox 镜像构建不出来,因此我直接验证同一条可达路径:把本次构建的 dist/cli.js 放进 Linux 容器,用 PR 的同款参数经 host.docker.internal 打宿主机上的 fake server(图 6)。三个 JSON 用例断言的字段全部产生:type:"result"、is_error:false、result:"Paris"、stats。宿主机账本显示预期的两次请求(主轮 + managed-auto-memory-extractor)。

PR 臂另外通过:tsc -p integration-tests/tsconfig.json、eslint、prettier --check。

发现

F1 —— 中等 —— 去掉 await delay(2000) 打开了一个约 300 ms 的 teardown 竞态(图 5)

所有断言都通过,运行仍以非零码退出:

 Test Files  1 passed (1)
      Tests  1 passed | 10 skipped (11)
⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
[Error: ENOTEMPTY: directory not empty, rmdir '.../acp-plan-mode-enforcement/.qwen-home']

归因 —— 各臂只差这个 settle 窗口:

臂 运行次数 断言 teardown ENOTEMPTY
PR 原样 45 45/45 通过 11(24%)
PR + 恢复 await delay(2000)(唯一差异) 20 20/20 通过 0
main 的断言 + PR 驱动 + delay(2000) 20 20/20 通过 0
整份 acp-integration.test.ts,PR 臂 6 11/11 ×6 0

机理(从 cleanup() 返回开始计时):+283 ms projects/<slug>/meta.json、+274 ms projects/<slug>/memory/MEMORY.md、+290 ms usage_record.jsonl,之后 10 s 内再无新文件。setupAcpTest 把 agent 的 QWEN_HOME 放在 rig.testDir 里面;cleanup() 虽然等到了 agent 退出,但之后仍有约 300 ms 的写入落在该目录。main 上那 2 秒等待(加上真实模型多秒的一轮)始终盖住了这个窗口;PR 这一轮 1.3 s 结束且不再等待,全局 teardown 的递归 rm 就可能落进窗口内。retry: 2 不会重试 teardown 错误。

影响:对现状的 CI 影响低 —— e2e.yml 按整文件分片,本用例后面还有两个用例,窗口会被盖住,整文件运行 0/6。但对评审者是实打实的:单独 -t 跑这个用例(也就是验证本 PR 最自然的方式)在 macOS 上约每四次红一次。

建议 —— 请不要简单把 sleep 加回去。 要么把 agent 的 QWEN_HOME 建在 rig.testDir 之外(mkdtempSync(join(tmpdir(), 'acp-home-')),在 cleanup() 里清理),要么让 cleanup() 等目录不再变化。两种做法都保住本 PR 换来的确定性;盲等 2 秒等于把 PR 想去掉的东西又加回来。

F2 —— 低,既有问题 —— json-output.test.ts 第 4 个用例仍绑定 CI secrets

should return a JSON error for enforced auth mismatch before running 从不调用模型,但仍然需要 OPENAI_API_KEY + OPENAI_BASE_URL + OPENAI_MODEL 三者都存在,CLI 才会把当前 auth 类型解析成 openai。少任何一个,两臂都失败:

AssertionError: expected 'No auth type is selected. Please conf…' to contain 'configured auth type is qwen-oauth'

不是回归 —— 但这是本文件里 PR 目标唯一没覆盖到的用例,用其他三例已经在用的 ...fakeModelArgs()(或显式设置这三个变量)即可收口。建议一并做掉,让整个文件真正自洽。

F3 / F4 —— nit

  • F3:两个文件里非流式分支({"selected_memories":[]})都从未被触发。插桩账本:ACP 用例只有 2 个请求且都是流式(#0 stream=true msgs=2 tools=30、#1 stream=true msgs=4);json-output.test.ts 共 6 个,全部流式。作为兜底无害,只是别把它当成覆盖了真实路径。
  • F4:streamingRequestIndex++ === 0 等于把"第一个流式请求就是用户这一轮"写死了。今天成立(上面已实测)。若将来在该轮之前插入一个流式 side query,脚本化的 write_file 会落到错误的请求上,失败现象会很难懂。改成对请求体做判断(例如最后一条消息是用户 prompt)会更耐改。

建议

修掉 F1 后可以合入。 这个 PR 的核心是对的,而且证据比描述写得更强 —— 它不只是稳住了一个 flaky 测试,而是替换掉了一个根本不会失败的测试。我唯一想卡的只有 F1,改动量是 setupAcpTest 里的几行。F2 建议顺手一起做;F3/F4 可选。

- Move per-agent QWEN_HOME out of rig.testDir: the agent keeps writing
  there ~300ms after exit, racing global teardown's recursive rm
  (ENOTEMPTY). cleanup() now removes it with retries.
- Drop the plan-mode test's permissionHandler: the plan-mode guard
  short-circuits before any permission request, so it could never run
  and its comment claimed coverage the test does not have.
- Name the collected tool-call events in the plan-mode assertion so a
  wire-shape drift is distinguishable from a plan-mode let-through.
- Stub loopback NO_PROXY in json-output tests: an inherited
  HTTP(S)_PROXY otherwise tunnels the fake-server POST and the cases
  time out. Two-chunk fixture restores multi-delta accumulator coverage.
- Make the auth-mismatch case hermetic via fakeModelArgs instead of the
  ambient OPENAI_* secrets.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

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

Autofix round summary — PR #11286

Commit: 4a3c61b270 (fix(test): close ACP teardown race and tighten fake-server E2E (#11271)), 2 files changed, +38/−22. Conflict resolution: none needed (--conflict false, no merge performed).

Feedback dispositions

Inline review findings (round 3)

  • [rc:3952640438] R3-1 — dead permissionHandler in the plan-mode test → Resolved (implemented as suggested). Deleted the option and its comment; the setupAcpTest default at :108-109 (() => ({ optionId: 'proceed_once' })) now applies. The handler was unreachable — the plan-mode guard short-circuits before any session/request_permission, and the fake model never emits switch_mode — and its comment claimed mode-switch-cancellation coverage the test does not have. Verified: the test stays green without it, and the guard's witness still holds (see probes).
  • [rc:3952640443] R3-2 — opaque load-bearing assertion → Resolved (suggestion block applied). expect(blockedEvent, ...) now names the collected toolCallEvents, so a wire-shape drift ("no frame collected") is distinguishable from a plan-mode let-through. Probe below reproduces the arm E→F flip. The optional catch-block dump was not added: the assertion message carries the discriminating information, and dumping sessionUpdates on every failure path adds noise without a demonstrated need.
  • [rc:3952640449] R3-3 — missing NO_PROXY on sandbox:none legs → Resolved (local-stub variant). beforeEach now does vi.stubEnv('NO_PROXY'/'no_proxy', IS_CONTAINER_SANDBOX ? CONTAINER_SANDBOX_NO_PROXY : '127.0.0.1,localhost') with vi.unstubAllEnvs() in afterEach, mirroring test-helper.ts:993-995. Chose the local fix over widening applyContainerSandboxNoProxy() to keep the four interactive-suite callers' behavior untouched and the diff inside this PR's footprint; the container value is unchanged (host.docker.internal preserved). A/B probe below: same dead-proxy environment, red (60s timeout) pre-fix → green in ~4s post-fix.
  • [rc:3952640452] R3-4 — single-delta fixture → Resolved (suggestion applied). Streamed branch now returns { contentChunks: ['Par', 'is'] }, restoring multi-delta accumulator coverage in the only suite exercising stream-json / --include-partial-messages. Mutation probe below reproduces arm D.

Maintainer verification report ([ic:5575224215], @wenshao)

  • F1 — teardown race (ENOTEMPTY on .qwen-home) → Resolved (maintainer's option 1, not a sleep). setupAcpTest now creates the per-agent QWEN_HOME via mkdtempSync(join(tmpdir(), 'qwen-acp-home-')) — outside rig.testDir, so the post-exit writes (measured ~300ms) can no longer race the global teardown's recursive rm of testDir. cleanup() removes the dir after waitForExit() with fs.rm(..., { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }), wrapped in a non-fatal warning catch (mirrors globalSetup.ts teardown: a cleanup that cannot finish must not turn an all-green run red). Evidence: 5/5 isolated --retry=0 runs green with no ENOTEMPTY, and zero leaked /tmp/qwen-acp-home-* dirs after the runs.
  • F2 — auth-mismatch case coupled to CI secrets → Resolved. The case now passes ...fakeModelArgs() like its siblings; the manual OPENAI_API_KEY env juggling is deleted. A/B probe below: with all three OPENAI_* vars unset, red pre-fix (No auth type is selected… — the exact message the maintainer quoted) → green post-fix. The file is now hermetic as a whole.
  • F3 — non-streaming stub branch never exercised → No change. The maintainer calls it a harmless safety net and asks only that it not be read as covering a live path; noted.
  • F4 — streamingRequestIndex++ === 0 ordering assumption → Declined (optional nit). The ordering is measured-stable today (both reviewers' ledgers: streaming request #0 is always the user turn). A request-body predicate is complexity against a hypothetical reorder, and if a streaming side query is ever added ahead of the turn, the new R3-2 assertion message now dumps the collected events, so the misroute fails diagnosably instead of opaquely.

Deferred (re-recorded for the follow-up queue)

  • [rc:3951510478] PR-time CI lane coverage for the two changed suites — already verified and agreed on the record (reply 3952125813). The fix lives in the e2e workflow matrix (.github/workflows/), outside this PR's footprint and outside the autofix loop's allowed areas. Re-recorded in deferred-findings.json so it survives merge.

Verification

Every command actually run, with its result (all from the repo root unless noted; integration runs used a fresh QWEN_HOME=$(mktemp -d) and QWEN_SANDBOX=false):

  • npm run build — passed
  • npm run typecheck — passed (workspaces + typecheck:integration, which is tsc -p integration-tests/tsconfig.json)
  • npm run lint — passed (eslint . --ext .ts,.tsx && eslint integration-tests)
  • npm run bundle — passed (regenerated dist/cli.js)
  • npx vitest run --root ./integration-tests cli/json-output.test.ts --retry=0 — 4 passed
  • same, under HTTP_PROXY=HTTPS_PROXY=http://127.0.0.1:9 with NO_PROXY/no_proxy unset — 4 passed in ~4.3s (R3-3 green arm)
  • npx vitest run --root ./integration-tests cli/json-output.test.ts -t 'enforced auth mismatch' --retry=0 with OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL unset — 1 passed (F2 green arm)
  • npx vitest run --root ./integration-tests cli/acp-integration.test.ts -t 'plan mode|usage metadata|auth is required' --retry=0 — 4 passed | 7 skipped
  • isolated plan-mode test ×5, --retry=0 — 5× 1 passed | 10 skipped, no ENOTEMPTY; 0 leaked /tmp/qwen-acp-home-* dirs afterwards (F1 evidence)
  • final re-run after bundle restore: json-output 4 passed, plan-mode 1 passed
  • pre-commit hook (scripts/pre-commit.js, lint-staged) — passed on the commit

Mutation probes (guard deleted/broken → focused tests must fail → restored → green):

  • R3-3 red arm (pre-fix code, this runner): dead-proxy env as above on the unmodified tree → should return a valid JSON array… failed by 60s test timeout (was ~1.5s healthy). Post-fix same env is green in ~4.3s — the A/B is the guard's witness.
  • F2 red arm (pre-fix code, this runner): env vars unset on the unmodified tree → failed with expected 'No auth type is selected. Please conf…' to contain 'configured auth type is qwen-oauth' — the exact coupling the maintainer reported. Post-fix green.
  • R3-2 probe: fixture mutant streamingRequestIndex++ === -1 (tool call never served) → the plan-mode test now fails with expected a failed write_file tool_call_update blocked by plan mode; events=[]: expected undefined to be defined, reproducing the reviewer's arm E→F flip; mutant reverted (grep-verified) and test re-run green.
  • R3-4 probe: bundle accumulator mutant (current.text += fragment → current.text = fragment in dist/chunks/chunk-QNWXBESC.js:562) with the committed two-chunk fixture → 3 failed, all expected 'is' to contain 'paris' — the keep-last-delta regression is now visible; chunk restored byte-identical (cmp) and the suite re-run green.
  • F1 probe: the race is timing-dependent (maintainer measured 24% per isolated run on macOS) and cannot be forced deterministically; evidence is 5/5 isolated green runs with zero leaked tmp dirs, plus the structural argument (late writes now land outside the tree the global teardown removes).
  • R3-1: dead-code deletion — the witness is the suite staying green without the handler, plus the maintainer's Run E teeth-check (flipping modeId to 'default' still fails fast via expect(blockedEvent).toBeDefined() rather than hanging on an unanswered permission request, since the default handler answers proceed_once).
中文说明

Autofix 本轮总结 — PR #11286

提交:4a3c61b270(fix(test): close ACP teardown race and tighten fake-server E2E (#11271)),改动 2 个文件,+38/−22。冲突处理:无需处理(--conflict false,未做合并)。

反馈处置

行内评审发现(第 3 轮)

  • [rc:3952640438] R3-1 — plan-mode 用例中的死 permissionHandler → 已解决(按建议实现)。 删除该选项及其注释,改由 setupAcpTest 在 :108-109 的默认值(() => ({ optionId: 'proceed_once' }))生效。该 handler 不可达——plan-mode 守卫在任何 session/request_permission 之前就已短路,fake model 也不会发出 switch_mode——而其注释却声称本用例覆盖了 mode-switch 取消,实际并未覆盖。已验证:删除后用例保持绿色,守卫的见证仍然成立(见下方探针)。
  • [rc:3952640443] R3-2 — 承重断言失败时不透明 → 已解决(应用建议代码块)。 expect(blockedEvent, ...) 现在会输出收集到的 toolCallEvents,从而把"没收集到帧"(线格式漂移)与"plan mode 放行写入"区分开。下方探针复现了评审者的 arm E→F 翻转。可选项"在 catch 中再转储一次"未采纳:断言消息已带有判别信息,而在每条失败路径上转储 sessionUpdates 只会增加噪音,没有已证实的需要。
  • [rc:3952640449] R3-3 — sandbox:none 腿上缺少 NO_PROXY → 已解决(采用本地 stub 方案)。 beforeEach 现在执行 vi.stubEnv('NO_PROXY'/'no_proxy', IS_CONTAINER_SANDBOX ? CONTAINER_SANDBOX_NO_PROXY : '127.0.0.1,localhost'),并在 afterEach 中 vi.unstubAllEnvs(),与 test-helper.ts:993-995 的写法一致。选择本地修复而非放宽 applyContainerSandboxNoProxy(),是为了不动另外四个 interactive 套件调用方的行为、并把 diff 留在本 PR 的足迹内;容器取值未变(保留 host.docker.internal)。下方 A/B 探针:同一 dead-proxy 环境下,修复前红(60 秒超时)→ 修复后约 4 秒绿。
  • [rc:3952640452] R3-4 — 单 delta fixture → 已解决(应用建议)。 流式分支现在返回 { contentChunks: ['Par', 'is'] },在这个唯一覆盖 stream-json / --include-partial-messages 的套件中恢复了多 delta 累加器覆盖。下方变异探针复现了 arm D。

维护者验证报告([ic:5575224215],@wenshao)

  • F1 — teardown 竞态(.qwen-home 上 ENOTEMPTY)→ 已解决(采用维护者的方案 1,而非加回 sleep)。 setupAcpTest 现在用 mkdtempSync(join(tmpdir(), 'qwen-acp-home-')) 创建每个 agent 的 QWEN_HOME——位于 rig.testDir 之外,进程退出后约 300ms 的迟写(已实测)从此不可能再与全局 teardown 对 testDir 的递归 rm 相争。cleanup() 在 waitForExit() 之后以 fs.rm(..., { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) 删除该目录,并包在非致命的告警 catch 中(对齐 globalSetup.ts teardown 的既定做法:清理失败不得把全绿的运行变红)。证据:5/5 次单独 --retry=0 运行全绿且无 ENOTEMPTY;运行后 /tmp/qwen-acp-home-* 零泄漏。
  • F2 — auth-mismatch 用例绑定 CI secrets → 已解决。 该用例现在与兄弟用例一样传入 ...fakeModelArgs();手工摆弄 OPENAI_API_KEY 环境变量的代码已删除。下方 A/B 探针:三个 OPENAI_* 变量全部取消设置时,修复前红(No auth type is selected…——正是维护者引用的报错)→ 修复后绿。整个文件现在完全自洽。
  • F3 — 非流式 stub 分支从未被触发 → 不改动。 维护者称其为无害的兜底,只要求别把它当作覆盖了真实路径;已记录。
  • F4 — streamingRequestIndex++ === 0 的顺序假设 → 拒绝(可选 nit)。 该顺序今天实测稳定(两位评审者的账本都显示流式请求 #0 总是用户这一轮)。为假设中的重排引入请求体断言属于多余的复杂度;而且若真有流式 side query 插到这一轮之前,新的 R3-2 断言消息会转储收集到的事件,错位会以可诊断的方式失败,而不是 opaque 失败。

延期(重新登记进后续队列)

  • [rc:3951510478] 两个被改套件的 PR-time CI 通道覆盖 —— 此前已验证并在记录上同意延期(reply 3952125813)。修复点在 e2e workflow 矩阵(.github/workflows/),超出本 PR 足迹,也超出 autofix 循环允许改动的区域。已重新写入 deferred-findings.json,确保合入后不丢失。

验证

实际执行过的每条命令及其结果(除注明外均在仓库根目录运行;集成测试均使用全新的 QWEN_HOME=$(mktemp -d) 与 QWEN_SANDBOX=false):

  • npm run build —— 通过
  • npm run typecheck —— 通过(workspaces + typecheck:integration,即 tsc -p integration-tests/tsconfig.json)
  • npm run lint —— 通过(eslint . --ext .ts,.tsx && eslint integration-tests)
  • npm run bundle —— 通过(重新生成 dist/cli.js)
  • npx vitest run --root ./integration-tests cli/json-output.test.ts --retry=0 —— 4 通过
  • 同上,但在 HTTP_PROXY=HTTPS_PROXY=http://127.0.0.1:9 且 NO_PROXY/no_proxy 未设置的环境下 —— 约 4.3 秒 4 通过(R3-3 绿色臂)
  • npx vitest run --root ./integration-tests cli/json-output.test.ts -t 'enforced auth mismatch' --retry=0,且 OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL 全部取消设置 —— 1 通过(F2 绿色臂)
  • npx vitest run --root ./integration-tests cli/acp-integration.test.ts -t 'plan mode|usage metadata|auth is required' --retry=0 —— 4 通过 | 7 跳过
  • 单独运行 plan-mode 用例 ×5(--retry=0)—— 5 次均 1 passed | 10 skipped,无 ENOTEMPTY;运行后 /tmp/qwen-acp-home-* 目录零泄漏(F1 证据)
  • bundle 还原后的最终复跑:json-output 4 通过,plan-mode 1 通过
  • pre-commit 钩子(scripts/pre-commit.js,lint-staged)—— 提交时通过

变异探针(删除/破坏守卫 → 聚焦测试必须失败 → 还原 → 复绿):

  • R3-3 红色臂(修复前代码,本机实测): 在未改动的树上施加上述 dead-proxy 环境 → should return a valid JSON array… 以 60 秒测试超时失败(健康时约 1.5 秒)。修复后同环境约 4.3 秒转绿——该 A/B 即守卫的见证。
  • F2 红色臂(修复前代码,本机实测): 在未改动的树上取消三个环境变量 → 以 expected 'No auth type is selected. Please conf…' to contain 'configured auth type is qwen-oauth' 失败——正是维护者报告的耦合。修复后转绿。
  • R3-2 探针: fixture 变异 streamingRequestIndex++ === -1(永不下发工具调用)→ plan-mode 用例现在以 expected a failed write_file tool_call_update blocked by plan mode; events=[]: expected undefined to be defined 失败,复现评审者的 arm E→F 翻转;变异已还原(grep 验证)并复跑转绿。
  • R3-4 探针: bundle 累加器变异(dist/chunks/chunk-QNWXBESC.js:562 中 current.text += fragment 改为 current.text = fragment)配合已提交的两 chunk fixture → 3 个失败,全部为 expected 'is' to contain 'paris'——"只保留最后一个 delta"的回归现在可见;chunk 已逐字节还原(cmp 验证)并复跑转绿。
  • F1 探针: 该竞态与时序相关(维护者在 macOS 上实测每次单独运行约 24%),无法确定性强制触发;证据为 5/5 次单独运行全绿、临时目录零泄漏,加上结构性论证(迟写现在落在全局 teardown 要删除的目录树之外)。
  • R3-1: 死代码删除——其见证是删除 handler 后套件保持绿色,外加维护者的 Run E"牙齿检查"(把 modeId 翻成 'default' 仍会通过 expect(blockedEvent).toBeDefined() 快速失败,而不是卡在无人应答的权限请求上,因为默认 handler 会应答 proceed_once)。

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

🧵 Resolved all 4 selected review thread(s). · 已关闭全部选中的 4 条评审线程。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

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

Address review — round 4 (commit d791032)

Both round-4 findings were Suggestion-level, verified against the code, and implemented. No conflicts with main were merged (--conflict false).

Feedback points and decisions

[rc:3953160954] R4-1 — ACP homes orphaned outside the tmpdir sweeper — IMPLEMENTED

Verified: qwen-acp-home occurred exactly once repo-wide (the creation site), and sweepLeakedQwenHomes() filters on qwen-e2e-home-, so a home whose best-effort removal gave up (or whose worker was torn down before cleanup() ran) was orphaned permanently. Before the hunk, abandoned homes landed under runDir, which globalSetup.teardown() removes — the leak was bounded.

Fix, staying inside the sweeper's own guards (24h age floor, exact-path exclusion of the live hermetic home, ownsQwenHome gate):

  • New pure-node:* module integration-tests/scratch-dir.ts exports ACP_HOME_PREFIX = 'qwen-e2e-home-acp-', nested under the sweeper's prefix. The constant is the single source of the name: the creation site in cli/acp-integration.test.ts builds the home from it, and the witness test plants from it, so the two can never drift silently. Per the finding, HERMETIC_HOME_PREFIX is not imported from globalSetup.js (that module assigns QWEN_HOME at import time).
  • Witness added to globalSetup.test.ts → describe('globalSetup hermetic qwen home'): plants a ${ACP_HOME_PREFIX}leaked dir in the redirected TMPDIR, backdates its mtime 48h past the cutoff with utimes, runs the real setup(), and asserts the dir is swept.

Mutation probe (required red check): temporarily reverting ACP_HOME_PREFIX to qwen-acp-home- made the witness test fail (Tests 1 failed | 4 skipped); restoring it returned the suite to green.

[rc:3953160964] R4-2 — drifted duplicate of the warn-and-swallow retried rm — IMPLEMENTED

Verified: the two blocks were byte-identical except maxRetries: 10 (ACP) vs maxRetries: 5 (globalSetup.ts), with the rationale comment duplicated.

Fix:

  • scratch-dir.ts also exports removeScratchDir(dir), the single home for the policy: rm(dir, { recursive, force, maxRetries: 5, retryDelay: 200 }), warn-and-swallow, with the Main CI failed: E2E Tests on 1637aa968b3f #10325 rationale comment living there once. The budget is a fixed constant rather than a defaulted parameter so the two call sites cannot drift apart again; 5 retries spans ~3s of backoff plus walk time, roughly 10× the ~300ms late-write window measured at the ACP site, so nothing the 10 covered is lost.
  • globalSetup.ts teardown and every cleanup() in cli/acp-integration.test.ts (11 setupAcpTest call sites share the one cleanup) now call it. The duplicated rationale comments were removed, not copied.

The helper cannot live in test-helper.ts (imports @lydell/node-pty, strip-ansi, vitest) nor be exported from globalSetup.ts (import-time QWEN_HOME side effect) — hence the new node:*-only module, exactly the constraint the finding named.

Probe note, honestly recorded: mutating the helper's catch to rethrow did not red the pre-existing refilling-writer test — its writer self-stops at 30s and rm's retry attempts span past that, so rm succeeds and the swallow branch is never reached. The never-throw contract therefore had no witness. Added integration-tests/scratch-dir.test.ts: removing a child of a regular file rejects with ENOTDIR (outside rm's retryable codes, so no timing dependence), and the test asserts removeScratchDir still resolves and warns. Probe re-run against it: rethrow mutation → test failed (vitest exit 1); restore → green.

Changes

  • integration-tests/scratch-dir.ts (new) — ACP_HOME_PREFIX, removeScratchDir with the unified budget and rationale.
  • integration-tests/scratch-dir.test.ts (new) — pins the never-throw/warn contract via a deterministic ENOTDIR rejection.
  • integration-tests/cli/acp-integration.test.ts — mkdtempSync(join(tmpdir(), ACP_HOME_PREFIX)); cleanup() delegates to removeScratchDir; local rm import dropped.
  • integration-tests/globalSetup.ts — teardown's inline try/catch rm replaced by removeScratchDir(hermeticQwenHome); rationale comment moved to the helper.
  • integration-tests/globalSetup.test.ts — sweeper witness for stale ACP homes (imports the same ACP_HOME_PREFIX the creation site uses).

No test was deleted or weakened. Net footprint stays inside the PR's own integration-tests/ area; the earlier-recorded deferral of PR-time CI lane coverage (comment 3951510478, reply 3952125813) is untouched and remains the follow-up.

Verification

  • npm run build — passed
  • npm run typecheck (workspaces + tsc -p integration-tests/tsconfig.json) — passed
  • npm run lint (eslint . --ext .ts,.tsx && eslint integration-tests) — passed
  • npx vitest run --root ./integration-tests globalSetup.test.ts scratch-dir.test.ts — 6 passed (includes the new sweeper witness and the never-throw pin)
  • npx vitest run --root ./integration-tests cli/acp-integration.test.ts -t 'blocks write tools in plan mode' — 1 passed; no qwen-e2e-home-acp-* leftover under tmpdir afterwards
  • Mutation probes — prefix reverted to qwen-acp-home- → witness test failed (then restored, green); helper catch mutated to rethrow → scratch-dir.test.ts failed with exit 1 (then restored, green)
  • Pre-commit hook (scripts/pre-commit.js lint-staged tasks) — passed on commit d791032

The full 11-test ACP suite and the CLI integration lane were not run locally: the remaining ACP tests require real model credentials unavailable in this environment, and that lane is skipped in CI — the fake-server-backed plan-mode test above is the in-scope acceptance for the changed cleanup() path.

中文说明

评审处理 — 第 4 轮(提交 d791032)

本轮两条发现均为 Suggestion 级,均已对照代码核实并实现。未与 main 做冲突合并(--conflict false)。

发现逐条处理

[rc:3953160954] R4-1 — ACP home 落在 tmpdir 清扫器之外而成为孤儿 — 已实现

核实:qwen-acp-home 全仓库只出现一次(即创建点),而 sweepLeakedQwenHomes() 以 qwen-e2e-home- 为过滤前缀,因此一旦尽力而为的删除放弃(或 worker 在 cleanup() 运行前被销毁),该目录就永久成为孤儿。在该 hunk 之前,被遗弃的目录落在 runDir 下,由 globalSetup.teardown() 删除——泄漏是有界的。

修复保持在清扫器自身守卫之内(24 小时年龄下限、按精确路径排除在用的 hermetic home、ownsQwenHome 门禁):

  • 新增只依赖 node:* 的模块 integration-tests/scratch-dir.ts,导出 ACP_HOME_PREFIX = 'qwen-e2e-home-acp-',嵌套在清扫器前缀之下。该常量是名字的唯一来源:cli/acp-integration.test.ts 的创建点用它构建目录名,见证测试也用它种植目录,两侧不可能再悄悄漂移。按发现要求,没有从 globalSetup.js 导入 HERMETIC_HOME_PREFIX(该模块在 import 时会赋值 QWEN_HOME)。
  • 在 globalSetup.test.ts 的 describe('globalSetup hermetic qwen home') 中新增见证:在被重定向的 TMPDIR 里种植 ${ACP_HOME_PREFIX}leaked 目录,用 utimes 把 mtime 回拨到 cutoff 之前 48 小时,运行真实的 setup(),断言该目录被清扫。

变异探针(必须的变红验证):临时把 ACP_HOME_PREFIX 改回 qwen-acp-home- 后,见证测试失败(Tests 1 failed | 4 skipped);恢复后套件转绿。

[rc:3953160964] R4-2 — 「告警后吞掉」的带重试 rm 出现漂移的重复拷贝 — 已实现

核实:两个代码块除 maxRetries: 10(ACP)对 maxRetries: 5(globalSetup.ts)外逐字相同,且理由注释被复制了两份。

修复:

  • scratch-dir.ts 同时导出 removeScratchDir(dir),作为该策略的唯一落点:rm(dir, { recursive, force, maxRetries: 5, retryDelay: 200 }),告警后吞掉,Main CI failed: E2E Tests on 1637aa968b3f #10325 的理由注释只在此保留一份。预算是固定常量而非带默认值的参数,两个调用点不可能再次漂移;5 次重试约覆盖 3 秒退避外加遍历时间,约为 ACP 现场实测 ~300ms 迟写窗口的 10 倍,原来 10 能覆盖的情形并未损失。
  • globalSetup.ts 的 teardown 与 cli/acp-integration.test.ts 中每个 cleanup()(11 处 setupAcpTest 调用点共用一个 cleanup)都改为调用它。被复制的理由注释是删除而非搬运。

helper 不能放进 test-helper.ts(它导入 @lydell/node-pty、strip-ansi、vitest),也不能从 globalSetup.ts 导出(import 时赋值 QWEN_HOME 的副作用)——因此新建只含 node:* 导入的模块,这正是发现指出的约束。

探针说明(如实记录):把 helper 的 catch 改成重新抛出后,既有的「目录持续回填」测试没有变红——它的写入者 30 秒后自行停止,而 rm 的重试跨越了这个时间点,rm 最终成功,吞掉异常的分支根本不会被走到。也就是说「永不抛出」这一契约此前没有见证。为此新增 integration-tests/scratch-dir.test.ts:删除一个普通文件的子路径会以 ENOTDIR 拒绝(不在 rm 的可重试错误码之内,因此没有时序依赖),测试断言 removeScratchDir 仍然正常返回并发出告警。针对它重跑探针:重新抛出的变异 → 测试失败(vitest 退出码 1);恢复 → 转绿。

变更内容

  • integration-tests/scratch-dir.ts(新增)—— ACP_HOME_PREFIX,以及带统一预算与理由注释的 removeScratchDir。
  • integration-tests/scratch-dir.test.ts(新增)—— 用确定性的 ENOTDIR 拒绝固定「永不抛出/告警」契约。
  • integration-tests/cli/acp-integration.test.ts —— mkdtempSync(join(tmpdir(), ACP_HOME_PREFIX));cleanup() 委托给 removeScratchDir;删除本地的 rm 导入。
  • integration-tests/globalSetup.ts —— teardown 内联的 try/catch rm 替换为 removeScratchDir(hermeticQwenHome);理由注释移入 helper。
  • integration-tests/globalSetup.test.ts —— 针对陈旧 ACP home 的清扫器见证(导入与创建点相同的 ACP_HOME_PREFIX)。

没有删除或削弱任何测试。净改动保持在 PR 自身的 integration-tests/ 范围内;此前已记录延后的「PR 时 CI 通道覆盖」(评论 3951510478,回复 3952125813)保持不动,仍属后续跟进项。

验证

  • npm run build —— 通过
  • npm run typecheck(workspaces + tsc -p integration-tests/tsconfig.json)—— 通过
  • npm run lint(eslint . --ext .ts,.tsx && eslint integration-tests)—— 通过
  • npx vitest run --root ./integration-tests globalSetup.test.ts scratch-dir.test.ts —— 6 个通过(含新增的清扫器见证与「永不抛出」固定测试)
  • npx vitest run --root ./integration-tests cli/acp-integration.test.ts -t 'blocks write tools in plan mode' —— 1 个通过;运行后 tmpdir 下无 qwen-e2e-home-acp-* 残留
  • 变异探针 —— 前缀改回 qwen-acp-home- → 见证测试失败(随后恢复并转绿);helper 的 catch 改为重新抛出 → scratch-dir.test.ts 失败、退出码 1(随后恢复并转绿)
  • pre-commit 钩子(scripts/pre-commit.js 的 lint-staged 任务)—— 在提交 d791032 上通过

完整的 11 个 ACP 测试与 CLI 集成通道未在本地运行:其余 ACP 测试需要本环境不可用的真实模型凭据,且该通道在 CI 中被跳过——上面基于 fake server 的 plan-mode 测试即为本次改动的 cleanup() 路径的范围内验收。

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

🧵 Resolved all 2 selected review thread(s). · 已关闭全部选中的 2 条评审线程。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@yiliang114
yiliang114 dismissed stale reviews from ghost September 8, 2026 02:45

Superseded by the current test-only rework; the criticized latency-policy files are no longer in the PR diff.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no code change

Outcome: no action. All feedback in this round is deferred by maintainer decision; the working tree is unchanged and nothing was committed.

What this round contained

The round-5 automated review posted four Suggestion-level inline findings. No Critical finding is open, no check is failing, and there is no base conflict. Maintainer @yiliang114 replied to each finding in its own thread, explicitly deferring all four under the repository review-round cap (after roughly five review rounds, land only Critical fixes and move remaining Suggestions to a follow-up — AGENTS.md).

Disposition of each finding

All four findings were verified against the code before deferral — they are real, so they are deferred (recorded for the follow-up queue), not declined:

  1. acp-integration.test.ts:132 — comment names only one cleanup race ([rc:3953916440]) — Verified: the comment cites only the global teardown's rm(runDir), which KEEP_OUTPUT: 'true' gates off on every CI lane; the un-retried TestRig.setup() reset rm (test-helper.ts:233) is the racer on those lanes, and the local KEEP_OUTPUT-unset arm is real too. Deferred by maintainer ([rc:3953951060]).
  2. scratch-dir.test.ts:26 — retry budget unpinned ([rc:3953916454]) — Verified: the only test pins the swallow-and-warn contract via a non-retryable ENOTDIR, so maxRetries: 5, retryDelay: 200 survives mutation to 0 with the suite green. Deferred by maintainer ([rc:3953952412]).
  3. globalSetup.test.ts:153 — sweeper live-home floor unpinned ([rc:3953916460]) — Verified: the sweeper's only test covers just the deletion direction with a 48h-backdated fixture; nothing pins that an in-flight run's ~30-minute-old home survives the 24h mtime cutoff. Deferred by maintainer ([rc:3953953857]).
  4. acp-integration.test.ts:309 — home lifecycle unpinned ([rc:3953916463]) — Verified: setupAcpTest does not return qwenHome and no case asserts placement or post-cleanup() removal; deleting await removeScratchDir(qwenHome); keeps the suite 11/11 green while leaking one home per test. Deferred by maintainer ([rc:3953955126]).

Follow-up handling

Each finding is recorded in deferred-findings.json for upsert into the per-PR "Deferred review findings" issue so none is silently dropped at merge, and each thread received a reply stating the deferral and is left open for the maintainer. This matches the review's own convergence note: with no Critical open, merging and moving the remaining Suggestion threads to a follow-up issue is an available ending.

Verification

No code was changed this round, so no build/typecheck/lint/test run was applicable. Verification performed: each finding's claims were checked against the current source at HEAD (d791032ba5) — integration-tests/cli/acp-integration.test.ts, integration-tests/scratch-dir.ts, integration-tests/scratch-dir.test.ts — and the maintainer's four in-thread deferral replies were confirmed in the review metadata. Working tree was clean at entry and remains clean.

中文说明

Autofix 评审轮次 — 无代码变更

结果:无需处理。 本轮所有反馈均已由维护者决定暂缓;工作树未改动,也未提交任何内容。

本轮内容

第 5 轮自动评审发布了四条 Suggestion 级别的行内发现。没有未决的 Critical 发现,没有失败的检查,也不存在与基线的冲突。维护者 @yiliang114 已在每条发现所在的线程中逐一回复,依据仓库的评审轮次上限规则(约五轮评审后只合入 Critical 修复,其余 Suggestion 转入后续处理 —— 见 AGENTS.md)明确暂缓了全部四条。

各发现的处置

四条发现在暂缓前均已对照代码核实 —— 它们属实,因此处置为延期(记录进后续处理队列),而非拒绝:

  1. acp-integration.test.ts:132 —— 注释只写了一种清理竞争([rc:3953916440]) —— 已核实:注释只提到全局 teardown 的 rm(runDir),而它在每条 CI 流水线上都被 KEEP_OUTPUT: 'true' 关掉;在这些流水线上真正的竞争者是未带重试的 TestRig.setup() 重置删除(test-helper.ts:233),且本地未设置 KEEP_OUTPUT 的一臂同样真实存在。维护者已暂缓([rc:3953951060])。
  2. scratch-dir.test.ts:26 —— 重试预算未被固定([rc:3953916454]) —— 已核实:唯一的测试通过不可重试的 ENOTDIR 只固定了"吞掉异常并告警"的契约,因此 maxRetries: 5, retryDelay: 200 被变异为 0 时套件仍全绿。维护者已暂缓([rc:3953952412])。
  3. globalSetup.test.ts:153 —— 清扫器存活 home 的下限未被固定([rc:3953916460]) —— 已核实:清扫器唯一的测试只覆盖"删除"方向,fixture 回拨 48 小时;没有任何断言固定"仍在运行中的 run 的约 30 分钟前的 home 会被 24 小时 mtime 下限保留"。维护者已暂缓([rc:3953953857])。
  4. acp-integration.test.ts:309 —— home 生命周期未被固定([rc:3953916463]) —— 已核实:setupAcpTest 不返回 qwenHome,也没有任何用例断言落点或 cleanup() 之后的删除;删掉 await removeScratchDir(qwenHome); 后套件仍 11/11 全绿,同时每个测试泄漏一个 home 目录。维护者已暂缓([rc:3953955126])。

后续处理

每条发现都已记录进 deferred-findings.json,将由工作流写入本 PR 的"Deferred review findings"跟踪 issue,确保合入后不会悄然丢失;每个线程都收到了说明暂缓决定的回复,并保持未解决状态留给维护者。这与评审自身的收敛结论一致:在没有未决 Critical 的情况下,合入 PR 并把剩余 Suggestion 线程转入后续 issue 是一个可选的结束方式。

验证

本轮没有代码变更,因此不适用任何构建/类型检查/lint/测试运行。已执行的验证:在 HEAD(d791032ba5)上对照当前源码核查了每条发现的论断 —— integration-tests/cli/acp-integration.test.ts、integration-tests/scratch-dir.ts、integration-tests/scratch-dir.test.ts —— 并在评审元数据中确认了维护者的四条线程内暂缓回复。进入本轮时工作树为干净状态,现在仍保持干净。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.0

@chiga0 chiga0 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 findings.
Approval blockers: none.

Tier: Standard — test-only change; E2E test isolation and determinism fix.

Scope: integration-tests/ only; no production code changed.

Checked:

  • ACP_HOME_PREFIX nesting: qwen-e2e-home-acp- starts with HERMETIC_HOME_PREFIX (qwen-e2e-home-) at globalSetup.ts:57 → sweepLeakedQwenHomes reclaims leaked ACP homes. globalSetup.test.ts pins this with TMPDIR redirect so os.tmpdir() returns tmpRoot when the module is freshly imported. ✓
  • wenshao F1 (teardown race): setupAcpTest now places the agent QWEN_HOME in tmpdir() via mkdtempSync(join(tmpdir(), ACP_HOME_PREFIX)) and calls removeScratchDir(qwenHome) in cleanup() — moves it outside rig.testDir so the ~300 ms post-exit write window no longer races global teardown. ✓
  • wenshao F2 (auth mismatch test): fakeModelArgs() now passed to the auth-mismatch case, providing all three of OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL — test no longer depends on CI secrets. ✓
  • env option threading: setupAcpTest spreads options?.env into spawn at line 141; plan-mode test env vars reach the agent process. ✓
  • Assertion correctness (_meta.toolName, status: "failed", content[0].content.text): shape confirmed against wenshao's live capture and corroborated by transcript-replay.ts. Old if (writeFileEvents.length > 0) guard was structurally unreachable; new unconditional assertion with deterministic write_file injection is load-bearing. ✓
  • Timing: sendRequest resolves after the full agent turn; session/update notifications arrive on the same stdout readline stream ahead of the response. Removing delay(2000) is correct here (delay's job was to cover the post-exit write race, not notification ordering). ✓
  • fakeServer?.close(): optional chaining in afterEach handles the case where startFakeOpenAIServer rejects. ✓
  • removeScratchDir deduplication: extracted to scratch-dir.ts, used in both globalSetup.ts and acp-integration.test.ts. scratch-dir.test.ts verifies the non-fatal catch path with a deterministic ENOTDIR. ✓
  • NO_PROXY: vi.stubEnv / vi.unstubAllEnvs pattern is safe; replaces the previous applyContainerSandboxNoProxy manual restore. ✓
  • Cross-check vs yiliang114 P2/P3: P2 (integration lane not running this file) is informational; P3 (positional coupling on streamingRequestIndex) and P3 (fakeServer?.close()) — the latter is fixed; the former is acknowledged and deferred under the project's review-round cap. No items require re-opening.

Not covered: production code unchanged; Linux/Windows execution not validated locally, but the change uses mkdtempSync and join(tmpdir(), ...) which are platform-agnostic, and wenshao's verification ran the container path successfully.

Reviewed with AI assistance.

@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — PR #11286, round 2 (delta since 3af26895e7)

Second maintainer pass. My first report covered base-vs-PR at 3af26895e7; this one covers only what landed after it — 4a3c61b270 (round 3) and d791032ba5 (round 4) — and hunts for anything those two introduced. Same setup as before: standalone worktree, real bundled CLI (npm run build && npm run bundle → dist/cli.js), real qwen --acp agent processes, nothing about the product mocked.

One convenience makes the A/B exact: git diff 3af26895e7 d791032ba5 -- packages/ scripts/ package.json is empty, so a single bundle serves both arms and every difference measured below is test code and nothing else.

Verdict: both things I asked for on 3af26895e7 are fixed, and I can measure both. Merge-ready. One new non-blocking issue, which the F1 fix did not cause so much as expose — it is the same underlying defect, now surfacing as a temp-dir leak instead of a red run — plus two smaller notes.


1. F2 is closed — json-output.test.ts is hermetic now

Fig 1

With no OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL in the environment at all, the fourth case (enforced auth mismatch) fails on 3af26895e7 and passes on the PR head. Same box, same bundle, same fresh HOME/TMPDIR. That was the one case in the file the PR's stated goal did not reach; it does now.

2. R3-3's NO_PROXY stub buys more than the thread claims

Also in the figure above. I gave both arms a dead outbound proxy — a listener that accepts the connection and never answers, the shape a wedged CI or corporate egress proxy has — and left everything else identical:

Arm Result
3af26895e7 no test ever reported; I killed it at my own 600 s timeout
d791032ba5 Tests 4 passed (4), 9.99 s

Before this round the file called applyContainerSandboxNoProxy(), which is a no-op on every non-container lane, so an inherited proxy took the whole file down. Worth recording, because the reviewer thread treated this as a tidy-up.

3. F1 is closed structurally — and closing it showed what was actually wrong

The home now comes from mkdtempSync(join(tmpdir(), ACP_HOME_PREFIX)) instead of join(rig.testDir, '.qwen-home'), exactly the fix I asked for. Measured with KEEP_OUTPUT=true:

  • 3af26895e7: .integration-tests/<ts>/acp-plan-mode-enforcement/.qwen-home/ present and populated — inside the tree teardown recursively removes.
  • d791032ba5: no .qwen-home anywhere under runDir.

Honest caveat: the ENOTEMPTY rate I reported last round (11/45) did not reproduce on my box today — 0/25 unloaded and 0/20 pinned to one core, on the pre-fix arm. The structural argument stands regardless, and what follows is a stronger version of the same evidence.


N1 — non-blocking — the per-agent home is re-created right after removeScratchDir() removes it, so nearly every run leaks one into the OS temp dir

Fig 2

The spawned node dist/cli.js --acp is a wrapper: packages/cli/src/llm.tsx:729-731 relaunches the app in a child so it can be restarted internally. waitForExit() resolves on the wrapper's exit; the child is reparented to ppid=1 and keeps running. A 50 ms process sampler over one run:

PID 122654  ppid=122569 (vitest worker)   755.191 s → 757.359 s   ← spawn() target; waitForExit() resolves here
PID 122738  ppid=122654 → 1               755.718 s → 757.418 s + ← the real agent, still alive

A 5 ms watcher on TMPDIR over the same run:

4554 ms  CREATE  qwen-e2e-home-acp-7VDO1X
6767 ms  DELETE  qwen-e2e-home-acp-7VDO1X   ← removeScratchDir succeeds; no warning is printed
6778 ms  CREATE  qwen-e2e-home-acp-7VDO1X   ← +11 ms, re-created by the ppid=1 survivor

The /proc census taken at that second CREATE names the survivor directly, and the re-created home holds usage_record.jsonl. So the retries in removeScratchDir never fire — the rm works, the directory just comes back.

Fig 3

Arm CPU Runs Assertions homes left in TMPDIR
3af26895e7 16 cores / 1 core 25 / 20 all pass n/a — home lives in runDir
PR as submitted 16 cores 25 25/25 pass 25
PR as submitted 1 core 20 20/20 pass 10
PR + QWEN_CODE_NO_RELAUNCH: '1' 16 cores 10 10/10 pass 0
PR + QWEN_CODE_NO_RELAUNCH: '1' 1 core 12 12/12 pass 0

Whole-file runs accumulate 2 per run here (8 failed | 3 passed — see N3; the eight that die at authenticate never complete a turn, so on a green CI lane the count would be higher). Negative control: deleting await removeScratchDir(qwenHome); gives exactly one leak per run, monotone 1→2→3 — the call is load-bearing, it just loses the race with the survivor.

Why this is not blocking. Each home is 36 KB, mode drwx------, and holds no credentials (settings.json is only {"security":{"auth":{"selectedType":"openai"}}}). Nothing turns red. And round 4's own sweepLeakedQwenHomes() reclaims them — I verified the nesting and the age floor in §4.

Why it is still worth a line. The round-4 rationale describes the leak source as "a worker torn down mid-test never runs the cleanup". That is not the dominant case: the ordinary all-green path leaks too — 35/35 unloaded runs here, and 10/20 when pinned to a single core. So the 24 h sweeper is now carrying the normal path rather than acting as a safety net, and cleanup() returning does not mean the agent is gone.

Fix I validated: adding QWEN_CODE_NO_RELAUNCH: '1' to setupAcpTest's agent env — the escape hatch the CLI already honours (llm.tsx:461) — makes the spawned process be the agent, so waitForExit() means what the test assumes. 0 leaks across 22 runs on two load profiles, and the whole-file failure set is byte-identical (8 failed | 3 passed, same names). The tradeoff to weigh: it stops these cases exercising the relaunch wrapper, which is a real production path for an ACP client. If you would rather keep that covered, the other shape is to poll the home until it stops changing before removing it.


4. Round 4's sweeper checks out

Fig 4

  • The prefix nesting is genuinely pinned. Mutating ACP_HOME_PREFIX to qwen-acp-home- (outside HERMETIC_HOME_PREFIX) turns the new globalSetup.test.ts case red; as submitted it is green. The witness is not decorative.
  • The 24 h floor cannot reap a run in flight. The PR only tests the deletion direction, so I probed the other one against the real setup(): a home 30 minutes old survives, one 23 h 59 m old survives, one 25 h old is swept. A concurrent E2E run on the same host is safe.
  • What is not pinned (already reported as rc:3953916454 and deferred under the round cap — this just confirms the deferral is factually right): maxRetries: 5 → 0 in scratch-dir.ts leaves scratch-dir.test.ts green, because its only case pins the swallow-and-warn contract through a non-retryable ENOTDIR.

Also clean on the PR arm: tsc -p integration-tests/tsconfig.json exit 0, eslint --max-warnings 0 on all six changed files exit 0, prettier --check clean, globalSetup.test.ts 5/5, scratch-dir.test.ts 1/1, json-output.test.ts 4/4.


N2 — nit — the multi-delta fixture buys real coverage that nothing would miss

Fig 5

Round 3's { content: 'Paris' } → { contentChunks: ['Par', 'is'] } is a genuine improvement, measured on the wire through the real CLI: 1 content_block_delta (["Paris"]) becomes 2 (["Par","is"]). But json-output.test.ts:238 is expect(eventTypes).toContain('content_block_delta'), which one delta satisfies — reverting the fixture leaves all four cases green. One line closes it:

expect(eventTypes.filter((t) => t === 'content_block_delta').length).toBeGreaterThan(1);

N3 — context, not a defect — 8 of the 11 ACP cases still depend on the shared gateway

Running the whole file with no model credentials gives 8 failed | 3 passed — identically on both arms, same test names, so this is not a regression. The eight die at authenticate({methodId:'openai'}) → Internal error. Worth stating plainly because the PR title is "isolate protocol checks from model service": it isolates the one case that was breaking main, and the rest of the file is still exposed to exactly the outage this PR was opened for. That is a reasonable scope, not a gap in the change — just don't read the file as isolated.

Also checked

  • R3-1 (dropping permissionHandler) is safe. I instrumented the collector and ran the plan-mode case: zero session/request_permission frames. The plan-mode guard short-circuits before the permission round-trip, exactly as the round-3 report claims, so the default proceed_once handler is never consulted.
  • The Linux/container half. packages/cli/src/serve/sandbox.ts explicitly volume-mounts both the resolved QWEN_HOME (:503-505) and os.tmpdir() at the same path (:536), so moving the home into the temp dir does not put it out of the sandbox's reach. I could not finish building the repo's sandbox image inside this session's budget, so that half is a code trace, not an execution.

Recommendation

Merge. Both blocking-ish items from my first pass are fixed and the fixes hold up under measurement rather than inspection. N1 is real and I would take the one-line version of it, but it leaks 36 KB of non-sensitive scratch that this PR's own sweeper already reclaims — that is follow-up material, not a merge gate, especially with the review round cap in play. N2/N3 are notes.

Reproduction
git worktree add --detach wt11286 d791032ba5 && cd wt11286
# product source is identical to 3af26895e7, so one bundle serves both arms
npm install && npm run build && npm run bundle

# F2: no OPENAI_* set anywhere
HOME=$(mktemp -d) QWEN_SANDBOX=false \
  npx vitest run --root ./integration-tests cli/json-output.test.ts --retry=0
#   d791032ba5 -> 4 passed ; 3af26895e7 -> 1 failed (enforced auth mismatch)

# NO_PROXY: dead proxy that accepts and never answers
python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',39997));s.listen(50);
[s.accept() for _ in iter(int,1)]" &
HTTP_PROXY=http://127.0.0.1:39997 HTTPS_PROXY=http://127.0.0.1:39997 \
HOME=$(mktemp -d) QWEN_SANDBOX=false timeout 600 \
  npx vitest run --root ./integration-tests cli/json-output.test.ts --retry=0

# N1: leak count over N runs with a private TMPDIR
T=$(mktemp -d); for i in $(seq 1 10); do
  TMPDIR=$T HOME=$(mktemp -d) QWEN_SANDBOX=false \
    npx vitest run --root ./integration-tests cli/acp-integration.test.ts \
      --retry=0 -t 'blocks write tools in plan mode' >/dev/null 2>&1
done; ls -d $T/qwen-e2e-home-acp-* | wc -l     # 10 as submitted, 0 with QWEN_CODE_NO_RELAUNCH=1

# sweeper mutation
sed -i "s/'qwen-e2e-home-acp-'/'qwen-acp-home-'/" integration-tests/scratch-dir.ts
npx vitest run --root ./integration-tests globalSetup.test.ts -t 'sweeps a stale per-agent ACP home'
中文说明

PR #11286 本地验证报告 —— 第 2 轮(3af26895e7 之后的增量)

第二次以维护者身份验证。第一轮报告覆盖的是 3af26895e7 上的 base↔PR 对比;本轮只看之后新增的 4a3c61b270(round 3)和 d791032ba5(round 4),并排查这两轮是否引入新问题。环境与上轮相同:独立 worktree、真实打包 CLI(npm run build && npm run bundle → dist/cli.js)、真实 qwen --acp 进程,被测产品不打任何桩。

有一点让 A/B 变得非常干净:git diff 3af26895e7 d791032ba5 -- packages/ scripts/ package.json 为空,因此一份 bundle 可同时服务两臂,下文测出的每一处差异都只来自测试代码。

结论:我上一轮提出的两个问题都已修复,而且都能实测出来。可以合入。 新发现 1 个非阻塞问题——它与其说是 F1 修复引入的,不如说是被它暴露出来的:底层是同一个缺陷,只是从"跑红"变成了"临时目录泄漏"。另有 2 条较小的说明。


1. F2 已收口 —— json-output.test.ts 现在是自洽的

图 1

在环境里完全没有 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL 的前提下,第四个用例(enforced auth mismatch)在 3af26895e7 上失败、在 PR head 上通过。同一台机器、同一份 bundle、同样全新的 HOME/TMPDIR。这原本是本文件里 PR 目标唯一没覆盖到的用例,现在覆盖到了。

2. R3-3 的 NO_PROXY 改动价值高于评审线程里的定位

同见上图。我给两臂都配了一个死代理——接受连接但永不应答,正是 CI 或企业出口代理卡死时的形态——其余完全一致:

臂 结果
3af26895e7 一个用例都没有报告结果;被我自己的 600s 超时杀掉
d791032ba5 Tests 4 passed (4),9.99 秒

本轮之前该文件调用的是 applyContainerSandboxNoProxy(),它在所有非容器 lane 上是空操作,所以一个继承来的代理就能把整个文件拖死。值得记一笔,因为评审线程把它当成了顺手整理。

3. F1 已从结构上收口 —— 而且收口过程暴露了真正的问题

home 现在由 mkdtempSync(join(tmpdir(), ACP_HOME_PREFIX)) 创建,不再是 join(rig.testDir, '.qwen-home'),正是我上轮建议的做法。用 KEEP_OUTPUT=true 实测:

  • 3af26895e7:.integration-tests/<ts>/acp-plan-mode-enforcement/.qwen-home/ 存在且有内容 —— 就在 teardown 递归删除的那棵树里。
  • d791032ba5:runDir 下不存在任何 .qwen-home。

需要如实说明: 我上轮报告的 ENOTEMPTY 概率(11/45)今天在我这台机器上没有复现 —— 修复前那一臂无负载 0/25、单核绑定 0/20。结构性论证不受影响,而且下面的证据比概率统计更强。


N1 —— 非阻塞 —— removeScratchDir() 删掉 home 之后它立刻被重建,因此几乎每次运行都会往系统临时目录泄漏一个

图 2

测试 spawn 出来的 node dist/cli.js --acp 只是个外壳:packages/cli/src/llm.tsx:729-731 会把 app 重新拉起到子进程里,以便内部重启。waitForExit() 等到的是外壳的退出;子进程被 reparent 到 ppid=1 后继续存活。一次运行的 50ms 进程采样:

PID 122654  ppid=122569 (vitest worker)   755.191s → 757.359s   ← spawn() 的目标;waitForExit() 在此返回
PID 122738  ppid=122654 → 1               755.718s → 757.418s + ← 真正的 agent,仍然活着

同一次运行、对 TMPDIR 的 5ms 监视:

4554 ms  CREATE  qwen-e2e-home-acp-7VDO1X
6767 ms  DELETE  qwen-e2e-home-acp-7VDO1X   ← removeScratchDir 删除成功,没有打印任何告警
6778 ms  CREATE  qwen-e2e-home-acp-7VDO1X   ← +11ms,被 ppid=1 的幸存进程重建

在第二次 CREATE 时刻做的 /proc 普查直接点出了这个幸存进程,重建出来的 home 里放着 usage_record.jsonl。也就是说 removeScratchDir 的重试根本没被触发 —— rm 是成功的,只是目录又回来了。

图 3

臂 CPU 运行次数 断言 TMPDIR 中残留 home
3af26895e7 16 核 / 单核 25 / 20 全部通过 不适用 —— home 在 runDir 里
PR 原样 16 核 25 25/25 通过 25
PR 原样 单核 20 20/20 通过 10
PR + QWEN_CODE_NO_RELAUNCH: '1' 16 核 10 10/10 通过 0
PR + QWEN_CODE_NO_RELAUNCH: '1' 单核 12 12/12 通过 0

整文件运行在我这里每次累积 2 个(8 failed | 3 passed,见 N3;那 8 个在 authenticate 阶段就挂了,从未完成一轮对话,所以在 CI 全绿的 lane 上这个数字会更高)。负对照:删掉 await removeScratchDir(qwenHome); 后每次运行恰好泄漏 1 个,单调 1→2→3 —— 这行是承重的,只是它跑不赢那个幸存进程。

为什么不阻塞。 每个 home 36KB,权限 drwx------,不含任何凭证(settings.json 只有 {"security":{"auth":{"selectedType":"openai"}}})。不会让任何用例变红。而且 round 4 自己加的 sweepLeakedQwenHomes() 会回收它们 —— 嵌套前缀和年龄下限我在第 4 节都验过了。

但仍然值得写一行。 round 4 的说明把泄漏来源描述为"worker 在测试中途被拆掉,cleanup 没跑"。这并不是主要情形:全绿的正常路径同样会泄漏,在我这里无负载 35/35 次运行全部泄漏,单核绑定时 10/20。也就是说那个 24 小时清扫器现在承担的是常规路径,而不是兜底;并且 cleanup() 返回并不代表 agent 已经消失。

我实测通过的修法: 在 setupAcpTest 的 agent env 里加 QWEN_CODE_NO_RELAUNCH: '1' —— CLI 本来就认这个开关(llm.tsx:461)—— 这样 spawn 出来的进程就是 agent 本身,waitForExit() 才是测试以为的那个语义。两种负载下共 22 次运行零泄漏,整文件失败集合逐条一致(8 failed | 3 passed,同名)。需要权衡的代价:这些用例将不再覆盖 relaunch 外壳,而对 ACP 客户端来说那是真实的生产路径。如果希望保留这层覆盖,另一种形态是在删除前轮询 home 直到它不再变化。


4. round 4 的清扫器没问题

图 4

  • 前缀嵌套是真的被钉住了。 把 ACP_HOME_PREFIX 变异成 qwen-acp-home-(跑出 HERMETIC_HOME_PREFIX 之外)会让新增的 globalSetup.test.ts 用例变红;原样则绿。这个见证不是摆设。
  • 24 小时下限不会误删在跑的运行。 PR 只测了删除方向,我对着真实的 setup() 补测了另一个方向:30 分钟前的 home 存活、23 小时 59 分的存活、25 小时的被清扫。同宿主机上并发的 E2E 运行是安全的。
  • 没有被钉住的部分(已在 rc:3953916454 报告过并按轮次上限延后处理 —— 这里只是确认该延后判断在事实层面成立):把 scratch-dir.ts 的 maxRetries: 5 改成 0,scratch-dir.test.ts 依然全绿,因为它唯一的用例是用不可重试的 ENOTDIR 去钉"吞掉异常并告警"这个契约。

PR 臂另外通过:tsc -p integration-tests/tsconfig.json 退出 0、六个改动文件的 eslint --max-warnings 0 退出 0、prettier --check 干净、globalSetup.test.ts 5/5、scratch-dir.test.ts 1/1、json-output.test.ts 4/4。


N2 —— nit —— 多 delta 夹具带来的覆盖是真的,但没有任何断言会发现它消失

图 5

round 3 把 { content: 'Paris' } 改成 { contentChunks: ['Par', 'is'] } 确实是改进,我在真实 CLI 上从线上量了:content_block_delta 从 1 个(["Paris"])变成 2 个(["Par","is"])。但 json-output.test.ts:238 是 expect(eventTypes).toContain('content_block_delta'),1 个就满足 —— 把夹具改回去,四个用例照样全绿。一行即可收口:

expect(eventTypes.filter((t) => t === 'content_block_delta').length).toBeGreaterThan(1);

N3 —— 是背景说明而非缺陷 —— 11 个 ACP 用例里仍有 8 个依赖共享网关

不带任何模型凭证跑整文件得到 8 failed | 3 passed —— 两臂完全一致、用例名相同,所以不是回归。这 8 个都挂在 authenticate({methodId:'openai'}) → Internal error。之所以要写明,是因为 PR 标题是"isolate protocol checks from model service":它隔离的是那个把 main 弄红的用例,文件里其余部分仍然暴露在本 PR 想解决的那类故障之下。这个范围是合理的,不算改动缺口 —— 只是别把整个文件当成已隔离。

另外核对过的

  • R3-1(删掉 permissionHandler)是安全的。 我给收集器插了桩再跑 plan mode 用例:session/request_permission 帧数为 0。plan mode 守卫在权限往返之前就短路了,与 round 3 报告的说法一致,所以默认的 proceed_once 处理器根本不会被用到。
  • Linux / 容器那一半。 packages/cli/src/serve/sandbox.ts 会显式把解析后的 QWEN_HOME(:503-505)和 os.tmpdir()(:536,映射到同一路径)挂进容器,所以把 home 挪到临时目录不会让它在 sandbox 里不可达。本次会话预算内我没能把仓库自带的 sandbox 镜像构建完,因此这一半是代码追踪,不是实跑。

建议

可以合入。 我上一轮提的两个问题都已修复,而且是实测站得住、不是看代码推断出来的。N1 是真问题,我个人会采纳那个一行版本的修法,但它泄漏的是 36KB 非敏感草稿数据,而且本 PR 自带的清扫器已经会回收 —— 属于后续跟进,不构成合入门槛,尤其是在评审轮次上限已经生效的情况下。N2/N3 是提示性说明。

🤖 Generated with Claude Code — Claude Opus 5 (1M context)

@yiliang114 yiliang114 changed the title test(e2e): isolate protocol checks from model service test(e2e): isolate protocol checks and ACP test state Sep 8, 2026

@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. The current head keeps the change test-only, covers both deterministic protocol checks and ACP scratch-home lifecycle, has no unresolved review threads, and the required CI checks pass.

@yiliang114
yiliang114 added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit e45ef59 Sep 8, 2026
56 of 57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Main CI failed: E2E Tests on f071516e18ad

4 participants