Skip to content

test(managed-agent): state which coordinator refusal paths are real - #13099

Merged
wenshao merged 2 commits into
mainfrom
test/13055-13056-coordinator-refusal-coverage
Sep 30, 2026
Merged

wenshao merged 2 commits into
mainfrom
test/13055-13056-coordinator-refusal-coverage

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the Hosted Turn coordinator's refusal tests say which production path each case covers. This is a test-only change for two #12955 review follow-ups.

  • Pre-submission refusals (test(managed-agent): clarify retryable Workspace refusal coverage #13055). The former four-row matrix mixed three different situations under one name. It is now split:
    • failsOnWorkspaceAuthorizationRefusalBeforeSubmission uses the real producer, WorkspaceExecutionStore.unavailable() (409, workspace_unavailable, not retryable), which the connector raises from createOrLoad authorization. The Turn fails at retry count 0 and 5.
    • retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission covers the same refusal on a claim that already recorded a submission attempt: it is retried even with the pre-admission budget spent.
    • retriesARetryableBrokerRefusalBeforeSubmissionDefensively keeps the isRetryable() clause covered, but as explicitly defensive coverage with a neutral fixture instead of a fabricated workspace_busy. No current producer raises a retryable RuntimeBrokerException before submission; WorkspaceExecutionStore.busy() is raised only on the Broker's tool-execution path.
    • The DaemonHttpException arm had no coverage in this file. Three cases now pin its classification: retriesAConflictReportedByTheHarness (a session-level Harness 409 such as hosted_turn_active is retried), failsAPermanentHarnessRejectionAfterSubmission (a 400 ends the Turn as hosted_harness_rejected even after a recorded submission) and retriesATransientHarnessFailureAfterSubmission (a 503 is retried). Broker lease contention (workspace_busy) does not reach this arm: the daemon ends the turn with a turn_error event, and a create-time 409 is answered by loading.
  • Post-submission uncertainty (test(managed-agent): clarify the post-submission refusal regression witness #13056). doesNotExhaustAfterSubmissionMayHaveBeenAdmitted is renamed neverTerminatesOnceSubmissionMayHaveBeenAdmitted and keeps both rows, including the original lost-response case. A comment records that deleting the RuntimeBrokerException arm keeps the refusal row green by design, and that the discriminating negative control is weakening its guard to drop !submissionAttempted.get().

Why it's needed

The matrix documented a coordinator-level retry of Broker lease contention that production does not take, and its name claimed "before submission" while one row covered a recorded submission. A reader, or a future change to a real throw site, could be validated against a fixture instead of the real contract. The post-submission row looked redundant under the wrong probe, which invited deleting a load-bearing witness.

Reviewer Test Plan

How to verify

  • HarnessCoordinatorTest runs in the normal Maven test phase for managed-agent-server; all cases should pass on main's production code.
  • Weakening the RuntimeBrokerException arm in HarnessCoordinator to !error.isRetryable() should fail neverTerminatesOnceSubmissionMayHaveBeenAdmitted[workspace refusal = true] and retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission.
  • Dropping the !error.isRetryable() clause should fail only retriesARetryableBrokerRefusalBeforeSubmissionDefensively.
  • Adding 409 to the DaemonHttpException "rejected" branch should fail retriesAConflictReportedByTheHarness; deleting the hosted_harness_rejected arm should fail failsAPermanentHarnessRejectionAfterSubmission; widening < 500 to < 600 should fail retriesATransientHarnessFailureAfterSubmission.

Evidence (Before & After)

N/A (tests only).

Tested on

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

Not built or run locally. I derived each case's expected outcome by reading HarnessCoordinator.dispatch and transientFailure on main. The mutation checks above have not been run.

Environment (optional)

N/A

Risk & Scope

  • Main risk or tradeoff: none for runtime behavior. Whether the !error.isRetryable() clause is deliberate defence or dead discrimination is left to the maintainers. This PR keeps and labels it rather than removing it.
  • Not validated / out of scope: mutation runs; the recovery policy for permanent refusals after possible admission (feat(managed-agent): define recovery visibility for post-submission Workspace refusal #13054), whose semantics this PR does not change.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #13055
Fixes #13056
Related: #13054

中文说明

这个 PR 做了什么

让 Hosted Turn 协调器的拒绝测试写清楚每个用例覆盖的是哪条生产路径。仅改测试,对应 #12955 评审的两个跟进项。

  • 提交前的拒绝(test(managed-agent): clarify retryable Workspace refusal coverage #13055)。 原来的四行矩阵在同一个名字下混合了三种情形,现在拆开:
    • failsOnWorkspaceAuthorizationRefusalBeforeSubmission 使用真实来源 WorkspaceExecutionStore.unavailable()(409、workspace_unavailable、不可重试),由 connector 在 createOrLoad 的授权中抛出。重试次数为 0 和 5 时 Turn 都失败。
    • retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission 覆盖同样的拒绝发生在已记录提交尝试的 claim 上:即使提交前的重试预算已用完,也会重试。
    • retriesARetryableBrokerRefusalBeforeSubmissionDefensively 继续覆盖 isRetryable() 条件,但明确标为防御性覆盖,并使用中性的 fixture,而不是伪造的 workspace_busy。目前没有任何来源会在提交前抛出可重试的 RuntimeBrokerException;WorkspaceExecutionStore.busy() 只在 Broker 的工具执行路径上抛出。
    • DaemonHttpException 分支在本文件中原先没有覆盖,现在由三个用例固定其分类:retriesAConflictReportedByTheHarness(会话级的 Harness 409,如 hosted_turn_active,会被重试)、failsAPermanentHarnessRejectionAfterSubmission(400 即使在已记录提交后也以 hosted_harness_rejected 结束 Turn)、retriesATransientHarnessFailureAfterSubmission(503 会被重试)。Broker 租约争用(workspace_busy)不会到达这个分支:daemon 以 turn_error 事件结束该 turn,而 create 阶段的 409 会被转为 load。
  • 提交后的不确定性(test(managed-agent): clarify the post-submission refusal regression witness #13056)。 doesNotExhaustAfterSubmissionMayHaveBeenAdmitted 更名为 neverTerminatesOnceSubmissionMayHaveBeenAdmitted,保留两行,包括原来的响应丢失用例。注释写明:删除 RuntimeBrokerException 分支按设计仍会让拒绝那一行通过,能区分的负向对照是把该分支的条件放宽、去掉 !submissionAttempted.get()。

为什么需要

原矩阵记录了一个生产中并不存在的"协调器层重试 Broker 租约争用",而且名字说的是"提交前",其中一行却覆盖了已记录提交的情形。读者或未来对真实抛出点的改动,可能会被拿去对照一个 fixture 而不是真实契约来验证。提交后的那一行在错误的探针下看起来冗余,容易导致删掉一个承重的见证。

评审验证方式

如何验证

  • HarnessCoordinatorTest 在 managed-agent-server 的常规 Maven 测试阶段运行,对 main 的生产代码应全部通过。
  • 把 HarnessCoordinator 中 RuntimeBrokerException 分支的条件放宽为 !error.isRetryable(),neverTerminatesOnceSubmissionMayHaveBeenAdmitted[workspace refusal = true] 和 retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission 应失败。
  • 去掉 !error.isRetryable() 条件,应只有 retriesARetryableBrokerRefusalBeforeSubmissionDefensively 失败。
  • 把 409 加入 DaemonHttpException 的"rejected"分支,retriesAConflictReportedByTheHarness 应失败;删掉 hosted_harness_rejected 分支,failsAPermanentHarnessRejectionAfterSubmission 应失败;把 < 500 放宽为 < 600,retriesATransientHarnessFailureAfterSubmission 应失败。

证据(前后对比)

N/A(仅测试)。

测试平台

本地未构建也未运行。每个用例的预期结果是通过阅读 main 上的 HarnessCoordinator.dispatch 和 transientFailure 推导出来的;上述变异检查尚未运行。

风险与范围

  • 主要风险或取舍:对运行时行为没有影响。!error.isRetryable() 条件是有意防御还是无效判断,留给维护者决定;本 PR 保留它并加以标注,而不是删除。
  • 未验证 / 不在范围内:变异运行;可能已被接纳之后的永久拒绝恢复策略(feat(managed-agent): define recovery visibility for post-submission Workspace refusal #13054),本 PR 不改变其语义。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #13055
Fixes #13056
Related: #13054

Follow-ups from the #12955 review (R3-6, R3-7), test-only:

- Split the pre-submission refusal matrix so each case names what it
  covers. A Workspace authorization refusal before submission uses the
  real WorkspaceExecutionStore.unavailable() and fails the Turn; the
  same refusal on a claim that already recorded a submission is retried.
  The retryable RuntimeBrokerException case is kept as explicitly
  defensive coverage with a neutral fixture, since no current producer
  raises one before submission and Broker lease contention does not
  reach that arm (#13055).
- Cover the path lease contention actually takes: a 409 reported by the
  Harness as DaemonHttpException is retried (#13055).
- Rename the post-submission test to the invariant it protects, keep
  both rows including the lost-response case, and record that weakening
  the RuntimeBrokerException arm's guard, not deleting the arm, is the
  negative control for the refusal row (#13056).

No production change. Not built or run locally.
…cells

Review follow-up on this PR:

- The comments claimed Broker lease contention (workspace_busy) reaches
  the coordinator as a DaemonHttpException 409. It does not: the daemon
  ends the turn with a turn_error event before any coordinator call can
  see it, and a create-time 409 is answered by loading. Describe the
  409 test as pinning session-level Harness conflicts instead.
- Pin the other two cells of the DaemonHttpException classification
  after a recorded submission: a 400 fails the Turn as
  hosted_harness_rejected, and a 503 is retried.
@yiliang114
yiliang114 dismissed a stale review via 697d38a September 30, 2026 12:27
wenshao added a commit to wenshao/qwen-code that referenced this pull request Sep 30, 2026
@wenshao

wenshao commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Local real-stack verification of 697d38a33e

Verdict: no blocker found, fine to merge. The PR changes one test file; the test plan's five mutation claims all hold when actually run, and each production path the tests and comments describe matched what a real Spring + MySQL + Hosted Harness + Broker stack did. One comment could be more precise (N1 below); it does not affect any assertion.

The head moved from 6cc7cdd8f9 to 697d38a33e while I was testing. Everything below is from 697d38a33e merged into main 3a8fd11711. The earlier head on main 3b18cfe5e4 gave the same outcome in every scenario it ran.

1. Unit run and mutation matrix

Check Result
HarnessCoordinatorTest at PR head (own base) 20 run, 0 failed; checkstyle 0 violations
HarnessCoordinatorTest on the merge with main 20 run, 0 failed
Whole managed-agent-server unit suite on the merge (mvn test) 245 run, 0 failed
Test-plan mutations (5) each fails exactly the tests the description names
Kill rate over 9 single-site mutants of HarnessCoordinator.java main's tests 6/9, this PR 9/9

Mutation matrix

The three mutants main cannot detect are the three DaemonHttpException cells (409 made terminal, the hosted_harness_rejected branch deleted, < 500 widened). The two that were listed as not yet run in the R1-2 thread are M7 and M8 here: both pass all 17 of main's tests and are killed by the test this PR adds for them. The comment on neverTerminatesOnceSubmissionMayHaveBeenAdmitted is also right: deleting the RuntimeBrokerException arm (M4) leaves that row green and is caught only by the pre-submission test, while dropping !submissionAttempted (M1) turns it red.

2. The same paths on a real stack

Setup: the packaged managed-agent-server jar with the embedded Runtime Broker, MySQL 8.4.7, the bundled CLI running as Hosted Harness, a recording proxy between Spring and the Harness, and a scripted OpenAI-compatible model. JDK Flight Recorder in the Spring JVM recorded where each exception was constructed, so the call paths below are observed, not read from source.

# Situation Observed Test it corresponds to
R1 Workspace drained before the first dispatch FAILED workspace_unavailable, retry 0, no Harness request, no retry failsOnWorkspaceAuthorizationRefusalBeforeSubmission[0]
R2 Harness unreachable until the budget is spent (5 retries, 17.9 s), then drained FAILED workspace_unavailable at retry 5, not hosted_harness_unavailable …BeforeSubmission[5]
R3 Turn admitted and still running; Workspace drained, stream cut retried 6 times in 34 s, past the budget of 5, never failed; COMPLETED 64 s after the Workspace was reactivated retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission, neverTerminates…
R4 Second Session on a Workspace whose lease is held FAILED hosted_turn_failed in about 1.7 s, retry 0; all three Harness calls returned 2xx the corrected comment: lease contention never reaches the coordinator
R6a Harness answers the submit with 400 FAILED hosted_harness_rejected, retry 0, submission already recorded failsAPermanentHarnessRejectionAfterSubmission
R6c Harness answers the submit with 409 hosted_turn_active one retry, then COMPLETED retriesAConflictReportedByTheHarness
R6b Event stream opens with 503 one retry, then COMPLETED retriesATransientHarnessFailureAfterSubmission

In R1 to R3 the refusal was constructed at WorkspaceExecutionStore.unavailable ← authorize ← QwenHostedHarnessConnector.createOrLoad ← HarnessCoordinator.runClaimed, which is the producer the first test comment names. In R4 workspace_busy was constructed at WorkspaceExecutionStore.claim ← WorkspaceRuntimeTransport.acquire on a Broker thread, reached the Harness as Runtime Broker returned HTTP 409 (workspace_busy), and ended the turn there. No retryable RuntimeBrokerException reached the coordinator in any scenario, which is consistent with labelling that case defensive.

Authorization refusal before submission

Refusal after a recorded submission

Broker lease contention

DaemonHttpException arm

3. Notes for the author (none blocking)

N1. The 409 comment names codes from other routes and leaves out the one that escapes createOrLoad. HarnessCoordinatorTest.java:138-139 says a create-time 409 never reaches the arm because the connector answers it by loading. That is true of the create call, but in two runs the answering load returned 409 as well, and that one did leave createOrLoad and hit this arm:

  • R6d, the reply to a successful create is lost: POST /session/:id/load → 409 hosted_session_already_attached, six times.
  • R5, Spring is killed and restarted mid-Turn: the same 409 on every re-dispatch.

This is the only 409 I saw reach the coordinator without injecting a status, and it is the shape the fixture uses (the mock throws from createOrLoad). Rewording its last sentence would make the comment match:

    // than failed. A create-time 409 is answered by loading the existing
    // authority; if that load conflicts too (hosted_session_already_attached),
    // it does reach here and is retried.

N2. Two fixtures throw from a call site production does not use. No change requested; the mutation results show both tests are load-bearing.

  • retriesATransientHarnessFailureAfterSubmission throws a 503 DaemonHttpException from createOrLoad. On the real stack a 5xx on create, load or submit is wrapped before it reaches the coordinator (R6e: a 503 on submit arrived as PromptAdmissionUnknownException and took the generic catch). Only the event-stream open produced a 5xx DaemonHttpException (R6b).
  • neverTerminates…[workspace refusal = true] throws the RuntimeBrokerException from submit, which is an HTTP call. The real post-submission producer is authorize on the next dispatch (R3), which the new recorded-submission test covers.

N3. The lease-contention comment is accurate for today's connector. Since #12894, main's Harness does relay workspace_busy as HTTP 409 on the load route (hosted-harness-session.ts:893, pinned by retries load after %s without losing the original committed Shell continuation, which passes here). It applies only to a committed Shell continuation, and the Spring connector requests only hosted-workspace-files/1, so the coordinator cannot reach it. If the connector adopts the Shell profile, that relay would land in the 409 cell this PR now pins, not in the RuntimeBrokerException arm.

4. Seen along the way, outside this PR

Both come from main's production code, which this PR does not touch. I found no existing issue mentioning hosted_session_already_attached and have not filed one.

  • R5. After Spring was killed and restarted (13.6 s) during a running Turn, every re-dispatch got 409 hosted_session_already_attached from the load route. The Turn was still RUNNING 22 minutes later after 27 retries, although the Session journal shows the Harness committed the turn's last message 100 s in. Retrying cannot succeed while the Harness keeps the Session attached.
  • R6d. One lost reply to a successful create made the Turn fail as hosted_harness_unavailable after 5 retries and 34.4 s, for the same reason.

5. Limits

  • Run on macOS 26.6 arm64 with JDK 21.0.12, Maven 3.9.16, Node 22.23.2 and MySQL 8.4.7. Linux and Windows are covered only by this PR's CI (21 checks passing on 697d38a33e; review-pr was still pending when I posted).
  • The model is scripted. R6a, R6b, R6c and R6e use statuses injected by the proxy; R1 uses a MySQL trigger to drain the Workspace in the statement that commits the Turn; R2 makes the Harness unreachable by dropping requests at the proxy. R3, R4, R5 and R6d inject no status.
  • Each scenario ran once per head, R3 twice on the current head.
  • The Shell-continuation relay in N3 was checked through its existing unit test, not reproduced end to end.

Rig scripts, per-scenario records and the matrix output: https://github.com/wenshao/qwen-code/tree/440d11f1f1e77cab79bafec17a5d5ae4427b14c0/pr13099

中文版

对 697d38a33e 的本地真实环境验证

结论:未发现阻塞问题,可以合并。 本 PR 只改了一个测试文件。测试计划里的五条变异断言实际运行后全部成立;测试和注释描述的每一条生产路径,都与真实的 Spring + MySQL + Hosted Harness + Broker 栈的行为一致。有一处注释可以写得更准确(见下文 N1),不影响任何断言。

验证途中 head 从 6cc7cdd8f9 变成了 697d38a33e。下面的结果全部来自 697d38a33e 合入 main 3a8fd11711 之后的树。此前在旧 head(main 3b18cfe5e4)上跑过的场景,结论完全一致。

1. 单元测试与变异矩阵

检查项 结果
PR head 上的 HarnessCoordinatorTest(自身 base) 20 个运行,0 失败;checkstyle 0 违规
与 main 合并后的 HarnessCoordinatorTest 20 个运行,0 失败
合并树上 managed-agent-server 全部单元测试(mvn test) 245 个运行,0 失败
测试计划中的 5 个变异 每个变异失败的测试与描述完全一致
HarnessCoordinator.java 9 个单点变异体的击杀率 main 的测试 6/9,本 PR 9/9

main 检测不到的三个变异体正是 DaemonHttpException 的三个分支(409 变成终态、删除 hosted_harness_rejected 分支、把 < 500 放宽)。R1-2 讨论里说尚未运行的两个变异对应这里的 M7 和 M8:两者在 main 的 17 个测试下全部通过,并被本 PR 为它们新增的测试击杀。neverTerminatesOnceSubmissionMayHaveBeenAdmitted 上方的注释也成立:删除 RuntimeBrokerException 分支(M4)后该行仍然通过,只有提交前的测试能发现;去掉 !submissionAttempted(M1)则会让它变红。

2. 真实环境中的同一批路径

环境:打包后的 managed-agent-server jar(内嵌 Runtime Broker)、MySQL 8.4.7、以 Hosted Harness 方式运行的打包 CLI、Spring 与 Harness 之间的记录代理,以及一个按脚本应答的 OpenAI 兼容模型。Spring JVM 里开启了 JDK Flight Recorder 记录每个异常的构造位置,所以下面的调用路径是实测到的,不是读源码推出来的。

# 场景 实测结果 对应的测试
R1 首次派发前 Workspace 被置为 DRAINING FAILED workspace_unavailable,重试 0 次,没有任何 Harness 请求 failsOnWorkspaceAuthorizationRefusalBeforeSubmission[0]
R2 Harness 不可达直到预算用完(5 次重试,17.9 s),然后 DRAINING 在重试计数 5 时 FAILED workspace_unavailable,而不是 hosted_harness_unavailable …BeforeSubmission[5]
R3 Turn 已被接纳且仍在执行;Workspace 置为 DRAINING 并切断事件流 34 s 内重试 6 次,超过预算 5,始终没有失败;Workspace 恢复 ACTIVE 后 64 s COMPLETED retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission、neverTerminates…
R4 租约被占用时,同一 Workspace 上的第二个 Session 约 1.7 s 后 FAILED hosted_turn_failed,重试 0 次;三次 Harness 调用全部返回 2xx 修正后的注释:租约争用到不了协调器
R6a Harness 对提交返回 400 FAILED hosted_harness_rejected,重试 0 次,提交已记录 failsAPermanentHarnessRejectionAfterSubmission
R6c Harness 对提交返回 409 hosted_turn_active 重试 1 次后 COMPLETED retriesAConflictReportedByTheHarness
R6b 事件流打开时返回 503 重试 1 次后 COMPLETED retriesATransientHarnessFailureAfterSubmission

R1 到 R3 中,拒绝异常构造于 WorkspaceExecutionStore.unavailable ← authorize ← QwenHostedHarnessConnector.createOrLoad ← HarnessCoordinator.runClaimed,正是第一条测试注释点名的来源。R4 中 workspace_busy 构造于 Broker 线程上的 WorkspaceExecutionStore.claim ← WorkspaceRuntimeTransport.acquire,以 Runtime Broker returned HTTP 409 (workspace_busy) 的形式到达 Harness,并在那里结束了该 turn。所有场景中都没有可重试的 RuntimeBrokerException 到达协调器,这与把该用例标注为防御性覆盖是一致的。

3. 给作者的说明(均不阻塞)

N1. 409 那条注释列的是其他路由的错误码,漏掉了真正会逃出 createOrLoad 的那个。 HarnessCoordinatorTest.java:138-139 说 create 阶段的 409 到不了这个分支,因为 connector 会改用 load 来应答。对 create 调用本身而言这是对的,但有两次运行里,用来应答的 load 也返回了 409,而这个 409 确实离开了 createOrLoad 并进入该分支:

  • R6d,一次成功 create 的应答丢失:POST /session/:id/load → 409 hosted_session_already_attached,共 6 次。
  • R5,Turn 执行中 Spring 被杀并重启:每次重新派发都得到同一个 409。

这是我在不注入状态码的情况下见到的唯一一个到达协调器的 409,也正是该 fixture 采用的形态(mock 从 createOrLoad 抛出)。改写最后一句就能让注释与实际一致(建议文字见英文部分)。

N2. 有两个 fixture 从生产代码不会用到的调用点抛出异常。 不要求修改,变异结果表明这两个测试都是承重的。

  • retriesATransientHarnessFailureAfterSubmission 从 createOrLoad 抛出 503 的 DaemonHttpException。真实环境中,create、load、submit 上的 5xx 在到达协调器之前就被包装了(R6e:提交时的 503 以 PromptAdmissionUnknownException 到达,走的是通用 catch)。只有打开事件流时才会产生 5xx 的 DaemonHttpException(R6b)。
  • neverTerminates…[workspace refusal = true] 从 submit 抛出 RuntimeBrokerException,而 submit 是一次 HTTP 调用。提交后真正的来源是下一次派发时的 authorize(R3),新增的"已记录提交"测试已经覆盖它。

N3. 租约争用那条注释对当前的 connector 是准确的。 自 #12894 起,main 上的 Harness 确实会在 load 路由上把 workspace_busy 转成 HTTP 409(hosted-harness-session.ts:893,由 retries load after %s without losing the original committed Shell continuation 固定,本地运行通过)。它只在已提交的 Shell 续跑时生效,而 Spring connector 只请求 hosted-workspace-files/1,所以协调器目前到不了这条路径。如果 connector 以后采用 Shell profile,这个转发会落在本 PR 新固定的 409 分支里,而不是 RuntimeBrokerException 分支。

4. 顺带观察到的、不属于本 PR 的现象

两者都来自 main 的生产代码,本 PR 没有改动它。我没有搜到提及 hosted_session_already_attached 的现有 Issue,也没有新建。

  • R5。 Turn 执行中 Spring 被杀并重启(13.6 s)之后,每次重新派发都从 load 路由得到 409 hosted_session_already_attached。22 分钟后该 Turn 仍是 RUNNING,已重试 27 次,而 Session journal 显示 Harness 在第 100 s 就提交了该 turn 的最后一条消息。只要 Harness 保持该 Session 的 attach 状态,重试就不可能成功。
  • R6d。 一次成功 create 的应答丢失,就让 Turn 在 5 次重试、34.4 s 之后以 hosted_harness_unavailable 失败,原因相同。

5. 局限

  • 运行环境为 macOS 26.6 arm64、JDK 21.0.12、Maven 3.9.16、Node 22.23.2、MySQL 8.4.7。Linux 和 Windows 只由本 PR 的 CI 覆盖(697d38a33e 上 21 项检查通过;发帖时 review-pr 仍在进行中)。
  • 模型是脚本化的。R6a、R6b、R6c、R6e 的状态码由代理注入;R1 用 MySQL 触发器在提交 Turn 的同一条语句里把 Workspace 置为 DRAINING;R2 通过代理丢弃请求来模拟 Harness 不可达。R3、R4、R5、R6d 没有注入任何状态码。
  • 每个场景在每个 head 上运行一次,R3 在当前 head 上运行了两次。
  • N3 中的 Shell 续跑转发只通过现有单元测试确认,没有端到端复现。

装置脚本、各场景记录和矩阵输出:https://github.com/wenshao/qwen-code/tree/440d11f1f1e77cab79bafec17a5d5ae4427b14c0/pr13099

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at head 697d38a33e270265ebee7c336aa72786ceec2bc0.

Verdict: APPROVE

No Critical has ever been filed on this PR — both automated rounds recorded only Suggestions — and my own scan of the complete diff found none. The change is a net fidelity improvement, not a weakened gate.

What I verified

The diff replaces one @CsvSource parameterized test whose four rows drove a single branchy if (expectRetry) … else … assertion with six focused tests plus a shared dispatchWithCreateOrLoadFailure(failure, submitted, retryCount) helper. Each new test asserts one classification cell and its negation, which is what makes them mutation-sensitive rather than vacuous:

  • failsOnWorkspaceAuthorizationRefusalBeforeSubmission (@ValueSource(ints = {0, 5})) pins failTurn(…, "workspace_unavailable", refusal.getMessage()) plus never() scheduleTurnRetry.
  • retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission pins the inverse for submitted=true, retryCount=5 — retry and never() failTurn.
  • retriesAConflictReportedByTheHarness pins the 409 cell of the DaemonHttpException arm; failsAPermanentHarnessRejectionAfterSubmission pins the non-409 4xx cell against hosted_harness_rejected; retriesATransientHarnessFailureAfterSubmission pins the 5xx cell. Together these are the three-way status classification that previously had only its 409-retry cell covered.
  • retriesARetryableBrokerRefusalBeforeSubmissionDefensively is explicitly labelled defensive, and the comment says outright that no current producer reaches the coordinator with a retryable RuntimeBrokerException before submission. Pinning the isRetryable() clause while stating that it models nothing real is the honest form of that test.

The mis-certification the first round reported is genuinely corrected rather than reworded. The group comment now states that workspace_busy is raised inside the Broker's tool-execution transport and consumed there, and that the 409 test pins session-level Harness conflicts (hosted_turn_active, hosted_prompt_conflict, hosted_event_epoch_mismatch) rather than a create-time conflict, which the connector answers by loading the existing authority. The old @CsvSource row that minted a RuntimeBrokerException(409, "workspace_busy", …) as a stand-in for Broker lease contention is gone, so no test now claims a path that does not exist.

Every test still ends with verify(harness, never()).submit(…), so the pre-submission boundary stays pinned across all six.

Non-blocking, and not part of the verdict

The one open thread I would flag to the author as worth a look, though it is graded Suggestion and does not gate: the helper stubs only the loadExisting=false arity of createOrLoad, while three of the six callers pass submitted=true — a state production reaches with loadExisting computed from session.harnessBootId() != null. So those three tests drive a state combination production does not produce. The classification logic they pin is still real and still mutation-sensitive, which is why this is fidelity rather than a false green.

CI

At this head 4 checks succeeded and 96 were skipped, with no failures and nothing pending. The lanes that would execute these Java tests report skipped, so nothing on the PR page evidences that the six new tests have run; the author also noted the two mutations were not run locally. I am not treating that as a blocker — this channel does not gate on CI state, and the assertions are readable as non-vacuous — but it is worth a maintainer confirming the Java lane executes them before merge.

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

Scan tier — LGTM

The CsvSource decomposition is accurate: each new test targets exactly one scenario, with the submitted / retryable / status-code axes independently exercised. The defensive test (retriesARetryableBrokerRefusalBeforeSubmissionDefensively) correctly pins the isRetryable() clause without overstating production coverage — the header comment is explicit that no current producer reaches this arm. The three DaemonHttpException rows (409 conflict, 400 permanent, 503 transient) are genuine new coverage consistent with the Harness contract described in the comments.

dispatchWithCreateOrLoadFailure returning store and leaving assertion to each caller is a better factoring than the original if-else: mutations that delete one arm no longer silently pass (the test fails with a missing interaction rather than the wrong branch executing).

The rename doesNotExhaustAfterSubmissionMayHaveBeenAdmitted → neverTerminatesOnceSubmissionMayHaveBeenAdmitted and @ParameterizedTest(name = "workspace refusal = {0}") improve CI failure messages for the post-submission invariant.

No issues found.

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

Scan tier — LGTM

The CsvSource decomposition is accurate: each new test targets exactly one scenario, with the submitted / retryable / status-code axes independently exercised. The defensive test (retriesARetryableBrokerRefusalBeforeSubmissionDefensively) correctly pins the isRetryable() clause without overstating production coverage — the header comment is explicit that no current producer reaches this arm. The three DaemonHttpException rows (409 conflict, 400 permanent, 503 transient) are genuine new coverage consistent with the Harness contract described in the comments.

dispatchWithCreateOrLoadFailure returning store and leaving assertion to each caller is a better factoring than the original if-else: mutations that delete one arm no longer silently pass (the test fails with a missing interaction rather than the wrong branch executing).

The rename doesNotExhaustAfterSubmissionMayHaveBeenAdmitted → neverTerminatesOnceSubmissionMayHaveBeenAdmitted and @ParameterizedTest(name = "workspace refusal = {0}") improve CI failure messages for the post-submission invariant.

No issues found.

@wenshao
wenshao added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit ed95678 Sep 30, 2026
202 of 205 checks passed

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 697d38a33e27. The change replaces one @CsvSource table with three named tests, and the naming is the substance: the old table encoded four boolean/int combinations without saying which correspond to a producer that exists.

What the new shape states explicitly, and what I checked:

  • failsOnWorkspaceAuthorizationRefusalBeforeSubmission pins the only refusal that actually reaches the coordinator pre-submission — WorkspaceExecutionStore.unavailable() (409, workspace_unavailable, not retryable) from the connector's authorization in createOrLoad — and asserts both halves: failTurn with that code, and scheduleTurnRetry never called.
  • retriesWorkspaceAuthorizationRefusalAfterARecordedSubmission pins the invariant the old table carried as an anonymous true, false, 0, true row: a claim that already recorded a submission attempt may have been admitted, so even a permanent refusal must not end it before the pre-admission budget. Naming it is what stops a future reader from "simplifying" it back into the permanent-failure arm.
  • retriesARetryableBrokerRefusalBeforeSubmissionDefensively is labelled defensive in its own comment ("no current producer reaches the coordinator with a retryable RuntimeBrokerException before submission"), so it pins the isRetryable() clause without claiming a path that does not exist. That is the right way to keep an unreachable-arm test.
  • The comment at :86-91 records why Broker lease contention is absent: workspace_busy is raised inside the Broker's tool-execution transport and consumed there as a turn_error, so it never reaches this arm.

Mechanical checks, since an import edit in Java is a compile error rather than a lint warning: CsvSource has 0 occurrences at this head, so dropping the import is correct; DaemonHttpException is imported at :17 and used at :142, :157, :171; dispatchWithCreateOrLoadFailure(RuntimeException, boolean, int) at :181-182 matches every new call site; new RuntimeBrokerException(409, "defensive_retryable", …) at :126 matches the (int, String, String, boolean) constructor. All of that is also settled empirically — CI is 22 pass / 0 fail with eight Java lanes green (ubuntu-latest JDK 11/17/21, macos-latest 21, windows-latest 21, Real daemon E2E 11, Runtime Broker + Managed Agent MariaDB 21, Hosted process fault gates MySQL 8.4 / 21).

Links #13055 and #13056. No findings.

Disclosure: I did not execute the Java suite; per the standing rule on this repo I do not run or patch Java locally. The verdict rests on reading the test plus the eight green Java lanes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(managed-agent): clarify the post-submission refusal regression witness test(managed-agent): clarify retryable Workspace refusal coverage

5 participants