Repository navigation
test(managed-agent): state which coordinator refusal paths are real - #13099
Conversation
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.
Local real-stack verification of
|
| 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 |
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.
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.
retriesATransientHarnessFailureAfterSubmissionthrows a 503DaemonHttpExceptionfromcreateOrLoad. On the real stack a 5xx on create, load or submit is wrapped before it reaches the coordinator (R6e: a 503 on submit arrived asPromptAdmissionUnknownExceptionand took the generic catch). Only the event-stream open produced a 5xxDaemonHttpException(R6b).neverTerminates…[workspace refusal = true]throws theRuntimeBrokerExceptionfromsubmit, which is an HTTP call. The real post-submission producer isauthorizeon 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_attachedfrom the load route. The Turn was stillRUNNING22 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_unavailableafter 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-prwas 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
left a comment
There was a problem hiding this comment.
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})) pinsfailTurn(…, "workspace_unavailable", refusal.getMessage())plusnever() scheduleTurnRetry.retriesWorkspaceAuthorizationRefusalAfterARecordedSubmissionpins the inverse forsubmitted=true, retryCount=5— retry andnever() failTurn.retriesAConflictReportedByTheHarnesspins the 409 cell of theDaemonHttpExceptionarm;failsAPermanentHarnessRejectionAfterSubmissionpins the non-409 4xx cell againsthosted_harness_rejected;retriesATransientHarnessFailureAfterSubmissionpins the 5xx cell. Together these are the three-way status classification that previously had only its 409-retry cell covered.retriesARetryableBrokerRefusalBeforeSubmissionDefensivelyis explicitly labelled defensive, and the comment says outright that no current producer reaches the coordinator with a retryableRuntimeBrokerExceptionbefore submission. Pinning theisRetryable()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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
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:
failsOnWorkspaceAuthorizationRefusalBeforeSubmissionpins the only refusal that actually reaches the coordinator pre-submission —WorkspaceExecutionStore.unavailable()(409,workspace_unavailable, not retryable) from the connector's authorization increateOrLoad— and asserts both halves:failTurnwith that code, andscheduleTurnRetrynever called.retriesWorkspaceAuthorizationRefusalAfterARecordedSubmissionpins the invariant the old table carried as an anonymoustrue, false, 0, truerow: 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.retriesARetryableBrokerRefusalBeforeSubmissionDefensivelyis labelled defensive in its own comment ("no current producer reaches the coordinator with a retryable RuntimeBrokerException before submission"), so it pins theisRetryable()clause without claiming a path that does not exist. That is the right way to keep an unreachable-arm test.- The comment at
:86-91records why Broker lease contention is absent:workspace_busyis raised inside the Broker's tool-execution transport and consumed there as aturn_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.





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.
failsOnWorkspaceAuthorizationRefusalBeforeSubmissionuses the real producer,WorkspaceExecutionStore.unavailable()(409,workspace_unavailable, not retryable), which the connector raises fromcreateOrLoadauthorization. The Turn fails at retry count 0 and 5.retriesWorkspaceAuthorizationRefusalAfterARecordedSubmissioncovers the same refusal on a claim that already recorded a submission attempt: it is retried even with the pre-admission budget spent.retriesARetryableBrokerRefusalBeforeSubmissionDefensivelykeeps theisRetryable()clause covered, but as explicitly defensive coverage with a neutral fixture instead of a fabricatedworkspace_busy. No current producer raises a retryableRuntimeBrokerExceptionbefore submission;WorkspaceExecutionStore.busy()is raised only on the Broker's tool-execution path.DaemonHttpExceptionarm had no coverage in this file. Three cases now pin its classification:retriesAConflictReportedByTheHarness(a session-level Harness 409 such ashosted_turn_activeis retried),failsAPermanentHarnessRejectionAfterSubmission(a 400 ends the Turn ashosted_harness_rejectedeven after a recorded submission) andretriesATransientHarnessFailureAfterSubmission(a 503 is retried). Broker lease contention (workspace_busy) does not reach this arm: the daemon ends the turn with aturn_errorevent, and a create-time 409 is answered by loading.doesNotExhaustAfterSubmissionMayHaveBeenAdmittedis renamedneverTerminatesOnceSubmissionMayHaveBeenAdmittedand keeps both rows, including the original lost-response case. A comment records that deleting theRuntimeBrokerExceptionarm 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
HarnessCoordinatorTestruns in the normal Maven test phase formanaged-agent-server; all cases should pass onmain's production code.RuntimeBrokerExceptionarm inHarnessCoordinatorto!error.isRetryable()should failneverTerminatesOnceSubmissionMayHaveBeenAdmitted[workspace refusal = true]andretriesWorkspaceAuthorizationRefusalAfterARecordedSubmission.!error.isRetryable()clause should fail onlyretriesARetryableBrokerRefusalBeforeSubmissionDefensively.DaemonHttpException"rejected" branch should failretriesAConflictReportedByTheHarness; deleting thehosted_harness_rejectedarm should failfailsAPermanentHarnessRejectionAfterSubmission; widening< 500to< 600should failretriesATransientHarnessFailureAfterSubmission.Evidence (Before & After)
N/A (tests only).
Tested on
Not built or run locally. I derived each case's expected outcome by reading
HarnessCoordinator.dispatchandtransientFailureonmain. The mutation checks above have not been run.Environment (optional)
N/A
Risk & Scope
!error.isRetryable()clause is deliberate defence or dead discrimination is left to the maintainers. This PR keeps and labels it rather than removing it.Linked Issues
Fixes #13055
Fixes #13056
Related: #13054
中文说明
这个 PR 做了什么
让 Hosted Turn 协调器的拒绝测试写清楚每个用例覆盖的是哪条生产路径。仅改测试,对应 #12955 评审的两个跟进项。
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。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失败。DaemonHttpException的"rejected"分支,retriesAConflictReportedByTheHarness应失败;删掉hosted_harness_rejected分支,failsAPermanentHarnessRejectionAfterSubmission应失败;把< 500放宽为< 600,retriesATransientHarnessFailureAfterSubmission应失败。证据(前后对比)
N/A(仅测试)。
测试平台
本地未构建也未运行。每个用例的预期结果是通过阅读
main上的HarnessCoordinator.dispatch和transientFailure推导出来的;上述变异检查尚未运行。风险与范围
!error.isRetryable()条件是有意防御还是无效判断,留给维护者决定;本 PR 保留它并加以标注,而不是删除。关联 Issue
Fixes #13055
Fixes #13056
Related: #13054