fix(test): reach fake model server from docker sandbox in provenance E2E (#7901) - #7903
fix(test): reach fake model server from docker sandbox in provenance E2E (#7901)#7903qwen-code-dev-bot wants to merge 3 commits into
Conversation
…E2E (#7901) The submitted-prompt-provenance interactive test bound its fake OpenAI server to 127.0.0.1 and handed the CLI a loopback base URL. Inside the docker sandbox the CLI runs in a container whose loopback is not the host, so the model turn never completed and the PROVENANCE_E2E_DONE wait timed out on every main run. Bind the server on 0.0.0.0 and point the containerized CLI at host.docker.internal (plus NO_PROXY), mirroring the protocol-tags and tool-control tests.
E2E Report — Issue #7901DiagnosisThe failing job is E2E Test (Linux) - sandbox:docker in the E2E Tests workflow run for commit This is not a regression from
Root causeThe test starts a fake OpenAI server and launches the CLI interactively against it. The fake server binds to FixApply the same container-sandbox networking pattern already used by
Verification
中文说明E2E 报告 — Issue #7901诊断失败的 job 是 commit 这不是
根因该测试启动一个 fake OpenAI 服务器,并以交互模式运行 CLI 连接它。fake 服务器默认绑定到 修复采用
验证
🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.7-max via Qwen Code /review
Main merged #7881, which fixes the same docker-sandbox networking issue and extracts the container-sandbox detection, NO_PROXY save/restore, and fake-server host options into integration-tests/test-helper.ts. Resolve the provenance-test conflict to main's helper-based version, dropping the inline duplication this branch originally added. This is exactly the de-duplication the PR review requested.
|
🤖 Addressed the latest review feedback (round 1/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/10 轮)。改动内容与我反驳保留之处如下: Review feedback addressed — PR #7903 (issue #7901)Feedback triageOne inline finding from the automated reviewer:
Classified as Optional (Suggestion) and resolved in the code. Decision and what changedInvestigation showed that
Because this round ran with Net effect: the branch now converges exactly to Conflict notes
Verification
中文说明已处理的审查反馈 — PR #7903(issue #7901)反馈分类自动化审查者提出的一条行内发现:
分类为 Optional(建议),并且 已在代码中解决。 决定与改动内容经调查发现,
由于本轮以 最终效果:本分支现在与 冲突说明
验证
Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。 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/模型 |
Verification report — local build + real container-sandbox E2EVerdict: the change this PR proposes is correct, but the branch no longer carries it. Its net diff against The PR body says the docker-sandbox path "could not be executed locally (no Docker available)" and relies on CI. I reproduced that environment locally with a real container sandbox (podman), so the fix itself is now verified end-to-end rather than by reasoning — it just turns out the verified code is already on 1. The PR's net diff against
|
| time (UTC) | event |
|---|---|
05:32:18 |
#7881 merged to main (4ca9d4960) — same fix, plus shared helpers in integration-tests/test-helper.ts |
05:40:28 |
#7903 commit 1 (7cc4d0479) — same fix, inline in the test file (+42/−10) |
06:46:59 |
#7903 commit 2 (7528de7c7) — merges main, resolves the conflict to main's version → net diff becomes empty |
Commit 2's own message states this. Two autofix PRs raced on issue #7901 and #7881 won; the correct resolution left nothing behind.
2. Root cause, reproduced directly
Before trusting the test result, I isolated the actual networking failure. Two host-side HTTP servers, then curl from a container started with the exact flag the sandbox uses (packages/cli/src/utils/sandbox.ts:458, --add-host host.docker.internal:host-gateway):
| # | server bind | URL used by containerized CLI | result |
|---|---|---|---|
| A | 127.0.0.1 |
http://127.0.0.1:PORT |
UNREACHABLE ← exactly the pre-#7881 test |
| B | 127.0.0.1 |
http://host.docker.internal:PORT |
UNREACHABLE |
| C | 0.0.0.0 |
http://host.docker.internal:PORT |
REACHED ← the fix |
Row B is the useful one: rewriting the host alone is not enough, and neither is binding wider alone. Both halves of the fix are load-bearing. This also confirms the PR's diagnosis that no timeout increase could ever have helped — the socket is unreachable, not slow.
3. A/B of the actual E2E test in a real container sandbox
Full 2×2: {pre-#7881 test file, main's test file} × {QWEN_SANDBOX=false, real podman container}. Every cell is a real vitest run driving the real bundled CLI.
| test-file version | QWEN_SANDBOX |
result | duration |
|---|---|---|---|
| pre-#7881 (loopback) | false (host) |
PASS | 2.38s |
| pre-#7881 (loopback) | podman (container) |
FAIL | 96.56s |
main (post-#7881) |
false (host) |
PASS | 2.14s |
main (post-#7881) |
podman (container) |
PASS | 2.72s |
The single failing cell fails with precisely the CI message from #7901:
AssertionError: Fake model turn did not complete: expected false to be true // Object.is equality
❯ interactive/submitted-prompt-provenance.test.ts:140:61
expect(completed, 'Fake model turn did not complete').toBe(true);
That is the exact reproduction of the red job, and it flips green with the fix. The 96.56s vs 2.72s gap is the two 30s waits burning down. The false (host) row explains why sandbox:none stayed green throughout — the bug is invisible outside a container.
I confirmed the CLI genuinely ran inside a container rather than silently falling back to the host (podman events shows a qwen-code-integration-test-* container starting from the sandbox image for each containerized run).
4. CI already agrees
| run | commit | sandbox:none |
sandbox:docker |
|---|---|---|---|
| 30329260667 (pre-#7881, filed as #7901) | b475d1a26 |
success | failure |
| 30333269444 (post-#7881) | 2db663bec |
success | success |
4ca9d4960 (#7881) is an ancestor of 2db663bec, and issue #7901 is already closed.
5. The original commit on this branch was correct
For fairness to the author: I also ran this branch's original inline version (7cc4d0479) in the container sandbox — 1 passed. The diagnosis and the fix were right; the branch simply lost a race, and commit 2 correctly de-duplicated rather than reintroducing the duplication. Nothing here is a quality problem.
6. One nuance worth recording (about main, not this PR)
applyContainerSandboxNoProxy() sets NO_PROXY/no_proxy in the test host process. Looking at sandbox.ts:309-328, those values are only forwarded into the container when QWEN_SANDBOX_PROXY_COMMAND is set, which the E2E workflow does not set. So in the docker job the NO_PROXY half is defensive for host-side requests only; the load-bearing half is the 0.0.0.0 bind + host.docker.internal base URL, as row B above shows. Harmless and worth keeping — just not what makes the job pass.
Recommendation
Close #7903 as superseded by #7881. Merging it would add an empty merge commit and no file change. The docker-sandbox E2E job is fixed and green on main, now verified by real execution rather than inference.
Harness details (how to reproduce)
- Worktree at
origin/mainb3873571a;npm ci && npm run build && npm run bundle. - Sandbox image:
ghcr.io/qwenlm/qwen-code:0.21.0with the locally builtdist/overlaid onto/usr/local/share/npm-global/lib/node_modules/@qwen-code/qwen-code/. The overlay is necessary because the published0.21.0image predates feat(hooks): Add submitted prompt provenance #7762, which added thesubmitted_promptprovenance feature the test asserts on — without it the container's CLI cannot satisfy the hook assertions. - Run:
QWEN_SANDBOX=podman QWEN_SANDBOX_IMAGE=localhost/qwen-sandbox-main:test npx vitest run --root ./integration-tests interactive/submitted-prompt-provenance.test.ts. - A/B by
git checkout 4ca9d4960^ -- integration-tests/interactive/submitted-prompt-provenance.test.tsand back.
Scope caveat: I used podman, not docker — docker is unavailable in this environment. The two share the code path that matters: IS_CONTAINER_SANDBOX covers both, and sandbox.ts:458 pushes --add-host host.docker.internal:host-gateway unconditionally for both. The only podman-specific branch in sandbox.ts is an --authfile optimisation (line 797). The full 2×2 matrix was run twice with identical results.
中文版报告
验证报告 —— 本地构建 + 真实容器沙箱 E2E
结论:本 PR 提出的改动是正确的,但分支上已经不再携带该改动。它相对 main 的净 diff 为空。建议作为 #7881 的重复项关闭,而不是合并。
PR 描述中提到 docker 沙箱路径"无法在本地执行(无可用 Docker)",只能依赖 CI。我在本地用真实容器沙箱(podman)复现了该环境,因此该修复本身现在是经过端到端实测验证的,而非仅靠推理 —— 只是被验证的这段代码已经在 main 上了。
1. 本 PR 相对 main 的净 diff 为空
gh pr view 7903显示 0 files changed、0 additions、0 deletions。git diff $(git merge-base origin/main <head>)..<head>→ 空。git merge-tree --write-tree origin/main <head>→6ab2ac0f32ab…,与origin/main^{tree}完全一致。今天合并本 PR 不会改动main上任何文件。
根据 commit 时间戳,事情的经过是:
| 时间 (UTC) | 事件 |
|---|---|
05:32:18 |
#7881 合入 main(4ca9d4960)—— 相同修复,并把辅助函数抽到 integration-tests/test-helper.ts |
05:40:28 |
#7903 commit 1(7cc4d0479)—— 相同修复,内联在测试文件中(+42/−10) |
06:46:59 |
#7903 commit 2(7528de7c7)—— merge main,把冲突解成 main 的版本 → 净 diff 变为空 |
commit 2 自己的信息也说明了这一点。两个 autofix PR 在 issue #7901 上撞车,#7881 先落地;正确的冲突解决方式使本分支不再剩下任何内容。
2. 直接复现根因
在采信测试结果之前,我先隔离了真正的网络故障。在宿主机上起两个 HTTP 服务,然后从一个使用沙箱完全相同参数(packages/cli/src/utils/sandbox.ts:458,--add-host host.docker.internal:host-gateway)启动的容器里 curl:
| # | 服务绑定 | 容器内 CLI 使用的 URL | 结果 |
|---|---|---|---|
| A | 127.0.0.1 |
http://127.0.0.1:PORT |
不可达 ← 正是 #7881 之前的测试 |
| B | 127.0.0.1 |
http://host.docker.internal:PORT |
不可达 |
| C | 0.0.0.0 |
http://host.docker.internal:PORT |
可达 ← 修复方案 |
B 行是关键:只改写 host 不够,只放宽绑定也不够,修复的两半都是必需的。这同时印证了 PR 的判断:增大超时时间永远无法修复 —— socket 是不可达,而不是慢。
3. 真实容器沙箱中对该 E2E 测试做 A/B
完整 2×2:{#7881 之前的测试文件,main 的测试文件} × {QWEN_SANDBOX=false,真实 podman 容器}。每个格子都是驱动真实打包 CLI 的真实 vitest 运行。
| 测试文件版本 | QWEN_SANDBOX |
结果 | 耗时 |
|---|---|---|---|
| #7881 之前(loopback) | false(宿主机) |
PASS | 2.38s |
| #7881 之前(loopback) | podman(容器) |
FAIL | 96.56s |
main(#7881 之后) |
false(宿主机) |
PASS | 2.14s |
main(#7881 之后) |
podman(容器) |
PASS | 2.72s |
唯一失败的格子,报错与 #7901 中的 CI 信息完全一致:
AssertionError: Fake model turn did not complete: expected false to be true // Object.is equality
❯ interactive/submitted-prompt-provenance.test.ts:140:61
expect(completed, 'Fake model turn did not complete').toBe(true);
这就是红色 job 的精确复现,并在应用修复后转绿。96.56s 与 2.72s 的差距正是两个 30s 等待耗尽的时间。false(宿主机)那两行解释了为什么 sandbox:none 一直是绿的 —— 该 bug 在容器外不可见。
我确认了 CLI 确实运行在容器内,而非静默回落到宿主机(podman events 显示每次容器化运行都从沙箱镜像启动了 qwen-code-integration-test-* 容器)。
4. CI 记录也印证了这一点
| run | commit | sandbox:none |
sandbox:docker |
|---|---|---|---|
| 30329260667(#7881 之前,即 #7901) | b475d1a26 |
success | failure |
| 30333269444(#7881 之后) | 2db663bec |
success | success |
4ca9d4960(#7881)是 2db663bec 的祖先,且 issue #7901 已关闭。
5. 本分支最初那个 commit 本身是正确的
为对作者公平:我也在容器沙箱中运行了本分支最初的内联版本(7cc4d0479)—— 1 passed。判断和修复都是对的;分支只是输掉了竞速,而 commit 2 正确地做了去重,没有重新引入重复代码。这里不存在质量问题。
6. 一个值得记录的细节(关于 main,与本 PR 无关)
applyContainerSandboxNoProxy() 设置的是测试宿主进程的 NO_PROXY/no_proxy。从 sandbox.ts:309-328 看,这些值只有在设置了 QWEN_SANDBOX_PROXY_COMMAND 时才会转发进容器,而 E2E workflow 并未设置它。所以在 docker job 中,NO_PROXY 这一半只对宿主侧请求起防御作用;真正起作用的是 0.0.0.0 绑定 + host.docker.internal base URL,如上面 B 行所示。这段代码无害、值得保留 —— 只是它并不是让 job 通过的原因。
建议
将 #7903 作为 #7881 的重复项关闭。 合并它只会增加一个空的 merge commit,不带来任何文件改动。docker 沙箱 E2E job 在 main 上已修复并转绿,且现在是通过真实执行而非推理验证的。
范围说明: 我使用的是 podman 而非 docker(本环境无 docker)。二者共享关键代码路径:IS_CONTAINER_SANDBOX 同时覆盖两者,sandbox.ts:458 对两者都无条件推入 --add-host host.docker.internal:host-gateway。sandbox.ts 中唯一的 podman 专属分支是第 797 行的 --authfile 优化。完整 2×2 矩阵运行了两遍,结果一致。
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Reviewed QwenLM/qwen-code#7903: no P0–P3 findings. The PR has zero remaining diff; the fix already landed via #7881 and CI is green. It should be closed as superseded. The GitHub posting action was cancelled, and I confirmed no review was published. No code changes were needed. ✅ completed |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenNo code changes were made this round. The PR's net diff against Both maintainers confirmed this in their reviews:
There are no inline comments, no review findings, and no failed checks to address. The appropriate next step is for a maintainer to close this PR. 中文说明未采取任何操作本轮未做任何代码改动。本 PR 相对 两位维护者在评审中均确认了这一点:
没有内联评论、没有评审发现、没有失败的检查需要处理。下一步应由维护者关闭本 PR。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
Superseded by #7881 — after merging main the net diff is empty. Closing. |



What this PR does
This PR fixes a consistently failing E2E test in the docker sandbox. The
submitted-prompt-provenanceinteractive test starts a fake OpenAI server and drives the CLI against it, but it bound that server to the host loopback interface and handed the CLI a loopback base URL. In the docker sandbox the CLI runs inside a container whose loopback is not the host, so the CLI could never reach the fake model and the turn-completion wait timed out on every run. The test now detects container sandbox mode and, only in that mode, binds the fake server on all interfaces, points the containerized CLI athost.docker.internal, and marks that host as no-proxy. The non-container path is left exactly as before.Why it's needed
The
E2E Test (Linux) - sandbox:dockerjob has failed on every recent main push, whilesandbox:noneand macOS pass. The failure is not a regression from any single commit — it reproduced identically across several unrelated commits — and it is not a timing flake: the fake server is simply unreachable from inside the container, so no timeout increase can fix it. This restores a reliable signal for the docker-sandbox E2E job and removes a persistent red check onmain.Reviewer Test Plan
The change is test-only and confined to the container-sandbox code path of one integration test. Confirm the test still passes outside a container and that the container path now mirrors the established pattern used by the protocol-tags and tool-control tests.
How to verify
Run the affected test outside a container (uses the bundled CLI and a fake server, no real model credentials needed):
npm run build && npm run bundle npx cross-env QWEN_SANDBOX=false npx vitest run --root ./integration-tests interactive/submitted-prompt-provenance.test.tsExpected:
1 passed. The full docker-sandbox verification happens in CI (E2E Test (Linux) - sandbox:docker), which is not reproducible in a local environment without Docker; the fix reuses the exact networking approach that already makesinteractive/protocol-tags-interactive.test.tsandsdk-typescript/tool-control.test.tsreach their fake servers from inside the container (the sandbox is started with--add-host host.docker.internal:host-gateway).Evidence (Before & After)
N/A — test-infrastructure change with no user-visible / TUI behavior. Before: the docker-sandbox job failed with
AssertionError: Fake model turn did not complete(timeout waiting forPROVENANCE_E2E_DONE). After: the containerized CLI reaches the fake server viahost.docker.internaland the turn completes.Tested on
Environment (optional)
Local:
npm run build && npm run bundle, then the integration test underQWEN_SANDBOX=false. Docker/Podman sandbox path verified by reasoning against the sandbox's--add-host host.docker.internal:host-gatewaysetup and the existing passing tests that use the same pattern; final confirmation is the CI docker job.Risk & Scope
QWEN_SANDBOXisdockerorpodman; in that mode it changes the fake server's bind address and the base URL host the CLI receives, matching two existing tests. Risk to other tests is nil because the change is local to this test file.Linked Issues
Fixes #7901
中文说明
本 PR 做了什么
本 PR 修复了一个在 docker 沙箱中持续失败的 E2E 测试。
submitted-prompt-provenance交互测试会启动一个 fake OpenAI 服务器并驱动 CLI 连接它,但它把该服务器绑定到宿主机 loopback 接口,并把 loopback base URL 传给 CLI。在 docker 沙箱中,CLI 运行在容器内部,其 loopback 并非宿主机,因此 CLI 永远无法访问 fake model,回合完成等待每次都超时。该测试现在会检测容器沙箱模式,并且仅在该模式下,将 fake 服务器绑定到所有接口、把容器内的 CLI 指向host.docker.internal,并把该 host 标记为 no-proxy。非容器路径保持完全不变。为什么需要
E2E Test (Linux) - sandbox:dockerjob 在最近每次 main push 上都失败,而sandbox:none和 macOS 通过。该失败不是任何单个 commit 的回归 —— 它在多个互不相关的 commit 上以完全相同的方式复现 —— 也不是时序不稳定:fake 服务器从容器内根本不可达,因此增大超时无法修复。这恢复了 docker 沙箱 E2E job 的可靠信号,并消除main上持续存在的红色检查。审阅者测试计划
改动仅涉及测试,且局限于一个集成测试的容器沙箱代码路径。确认该测试在容器外仍然通过,并且容器路径现在遵循 protocol-tags 和 tool-control 测试已使用的既有模式。
如何验证
在容器外运行受影响的测试(使用打包后的 CLI 和 fake 服务器,无需真实模型凭证):
npm run build && npm run bundle npx cross-env QWEN_SANDBOX=false npx vitest run --root ./integration-tests interactive/submitted-prompt-provenance.test.ts预期:
1 passed。完整的 docker 沙箱验证在 CI(E2E Test (Linux) - sandbox:docker)中进行,在没有 Docker 的本地环境无法复现;该修复复用了已经让interactive/protocol-tags-interactive.test.ts和sdk-typescript/tool-control.test.ts能从容器内访问其 fake 服务器的完全相同的网络方案(沙箱以--add-host host.docker.internal:host-gateway启动)。证据(Before & After)
N/A —— 测试基础设施改动,无用户可见 / TUI 行为。Before:docker 沙箱 job 以
AssertionError: Fake model turn did not complete(等待PROVENANCE_E2E_DONE超时)失败。After:容器内的 CLI 通过host.docker.internal访问 fake 服务器,回合完成。测试环境
环境(可选)
本地:
npm run build && npm run bundle,然后在QWEN_SANDBOX=false下运行集成测试。Docker/Podman 沙箱路径通过对照沙箱的--add-host host.docker.internal:host-gateway设置以及使用相同模式的既有通过测试进行推理验证;最终确认来自 CI 的 docker job。风险与范围
QWEN_SANDBOX为docker或podman时生效;在该模式下,它改变了 fake 服务器的绑定地址以及 CLI 收到的 base URL host,与两个既有测试一致。对其他测试的风险为零,因为改动局限于本测试文件。关联 Issue
Fixes #7901