Skip to content

fix(managed-agent): return 404 for unknown tool publications - #13602

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
ZedingZhang:fix/managed-agent-unknown-publication
Oct 11, 2026
Merged

wenshao merged 1 commit into
QwenLM:mainfrom
ZedingZhang:fix/managed-agent-unknown-publication

Conversation

@ZedingZhang

@ZedingZhang ZedingZhang commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Authenticated writer requests for an unknown tool publication now receive 404 managed_tool_publication_unknown from the finished-result, admission-prepare, and range-read routes. The two shared reads check for a missing record and return the existing API error envelope. Writer authentication still runs before the publication lookup.

Why it's needed

A stale or incorrect publication ID currently produces 500 internal_error after successful writer authentication. Recognizing a missing publication as a domain error gives the caller a consistent refusal and avoids logging an expected missing-resource case as an unexpected server exception.

Reviewer Test Plan

How to verify

  1. Establish a Session writer credential and acquire its writer lease. Request a publication ID that does not exist through GET …/finished, POST …/admissions/prepare, and POST …/range. Each response should be 404 with error.code = managed_tool_publication_unknown.
  2. Repeat those requests with an invalid writer credential. Each should still return 403 writer_credential_invalid before publication lookup.
  3. Confirm that an existing publication can still be finished, admitted, and read by range, including reading the tail of a large output after replacing the store instance.

Evidence (Before & After)

N/A — no user-facing UI change. HTTP integration checks used real migrated H2 data, the writer credential policy, Session/publication stores, controller mappings, and error handler. Only unused broker repositories and external object storage were mocked.

Request for a missing publication Before After
Authenticated GET …/finished 500 internal_error 404 managed_tool_publication_unknown
Authenticated POST …/admissions/prepare 500 internal_error 404 managed_tool_publication_unknown
Authenticated POST …/range 500 internal_error 404 managed_tool_publication_unknown
Invalid writer credential on all three routes 403 writer_credential_invalid 403 writer_credential_invalid

The same independent reproduction passed after the fix; all three cases failed with expected-404/actual-500 on unmodified upstream 464f486e. Permanent HTTP regression coverage has 7 passing cases, including the 6 new cases. The existing publication storage suite has 110 passing cases, and the permission/admission acceptance suite has 165 passing cases.

Java validation on base ad6039aa:

mvn --batch-mode --no-transfer-progress clean verify checkstyle:check
1268 tests: 1204 passed, 4 skipped, 1 failure, 59 errors

mvn --batch-mode --no-transfer-progress -DskipTests verify checkstyle:check
BUILD SUCCESS; 0 Checkstyle violations; 0 SpotBugs findings

The full Java test run is not green on this Windows host: the 60 failing cases are in six existing Workspace filesystem suites. Replaying those six suites on unmodified upstream ad6039aa reproduced exactly the same 60 failed case names and failure/error types (68 cases: 1 failure, 59 errors, 3 skipped). They require filesystem/mount behavior unavailable on this host.

Root npm run preflight passed formatting, lint, build, and typecheck. Its Node test phase is not green on this Windows host. All 14 failed ACP Workspace/symlink cases were reproduced on upstream ad6039aa (three replayed files: 1167 passed, 14 failed, 1 skipped). The unchanged Chrome extension ZIP symlink fixture also failed with Windows EPERM on both checkouts (base replay: 6 passed, 1 failed, 1 skipped). The remaining unrelated Node workspace tests were stopped after confirming these existing host failures; no overall preflight pass is claimed.

Tested on

OS Status
🍏 macOS ⚠️ not tested locally
🪟 Windows ✅ HTTP regressions, existing publication/acceptance suites, and static checks; full-suite limitations above
🐧 Linux ⚠️ not tested locally

Environment (optional)

Java 21.0.12.1, Maven 3.9.11, Node 24.19.0, and pnpm 11.24.0. Java tests ran in the Managed Agent server module with its SDK and Runtime Broker dependencies built from this checkout.

Risk & Scope

  • Main risk or tradeoff: the missing-publication response changes from a generic 500 to a stable 404; authentication, tenant/workspace/session predicates, and existing publication validation keep their current order.
  • Not validated / out of scope: real OSS, MySQL integration profiles, and local macOS/Linux execution. Existing Windows filesystem test failures are reported above.
  • Breaking changes / migration notes: no database migration or request-shape change; callers requesting a missing publication receive the new status and error code.

Linked Issues

Fixes #13563

中文说明

此 PR 的改动

已认证 writer 通过 finished-result、admission-prepare 和 range-read 三个接口请求不存在的 tool publication 时,统一收到 404 managed_tool_publication_unknown。两个共享读取点检查记录是否缺失,并返回已有的 API 错误响应结构。writer 认证仍在 publication 查询之前执行。

修改原因

过期或错误的 publication ID 目前会在 writer 认证成功后产生 500 internal_error。把缺失 publication 识别为领域错误,让调用方得到一致的拒绝响应,也避免将预期的资源缺失当成意外服务端异常记录。

审查者测试计划

验证方式

  1. 建立 Session writer 凭据并获取 writer 租约,分别通过 GET …/finished、POST …/admissions/prepare 和 POST …/range 请求一个不存在的 publication ID。每个响应应为 404,且 error.code = managed_tool_publication_unknown。
  2. 使用无效 writer 凭据重复这些请求。每个请求仍应在 publication 查询之前返回 403 writer_credential_invalid。
  3. 确认已有 publication 仍能完成、进行 admission 和按范围读取,包括更换 store 实例后读取大输出的尾部。

修改前后的证据

N/A — 没有面向用户的 UI 改动。HTTP 集成检查使用了真实迁移后的 H2 数据、writer 凭据策略、Session/publication stores、控制器映射和错误处理器。仅未被这些路径使用的 broker repositories 和外部对象存储使用了 mock。

请求不存在的 publication 修改前 修改后
已认证 GET …/finished 500 internal_error 404 managed_tool_publication_unknown
已认证 POST …/admissions/prepare 500 internal_error 404 managed_tool_publication_unknown
已认证 POST …/range 500 internal_error 404 managed_tool_publication_unknown
三个接口使用无效 writer 凭据 403 writer_credential_invalid 403 writer_credential_invalid

同一份独立复现脚本在修复后通过;三个案例在未修改的上游 464f486e 上均以 expected-404/actual-500 失败。正式 HTTP 回归测试共 7 个案例通过,其中 6 个为新增案例。已有 publication 存储测试的 110 个案例通过,权限/admission 验收测试的 165 个案例通过。

基于 ad6039aa 的 Java 验证:

mvn --batch-mode --no-transfer-progress clean verify checkstyle:check
1268 个测试:1204 通过,4 跳过,1 个断言失败,59 个执行错误

mvn --batch-mode --no-transfer-progress -DskipTests verify checkstyle:check
BUILD SUCCESS;0 个 Checkstyle 违规;0 个 SpotBugs 问题

全量 Java 测试在本 Windows 环境未全部通过:60 个失败案例来自六个已有的 Workspace 文件系统测试类。在未修改的上游 ad6039aa 上重跑这六个类,复现了完全一致的 60 个失败案例名称及 failure/error 类型(68 个案例:1 个断言失败,59 个执行错误,3 个跳过)。它们依赖本环境不支持的文件系统/挂载行为。

根目录 npm run preflight 的格式、lint、构建和类型检查通过。Node 测试阶段在本 Windows 环境未全部通过。14 个失败的 ACP Workspace/符号链接案例全部在上游 ad6039aa 复现(重跑的三个文件中,1167 个通过,14 个失败,1 个跳过)。未修改的 Chrome 扩展 ZIP 符号链接测试也在两个 checkout 上因 Windows EPERM 失败(上游重跑:6 个通过,1 个失败,1 个跳过)。确认这些已有环境失败后停止了余下不相关的 Node workspace 测试;这里不声称完整 preflight 通过。

测试平台

OS 状态
🍏 macOS ⚠️ 未本地测试
🪟 Windows ✅ HTTP 回归、已有 publication/准入测试及静态检查;全量测试限制见上文
🐧 Linux ⚠️ 未本地测试

环境(可选)

Java 21.0.12.1、Maven 3.9.11、Node 24.19.0 和 pnpm 11.24.0。Java 测试在 Managed Agent server 模块执行,其 SDK 和 Runtime Broker 依赖从本 checkout 构建。

风险与范围

  • 主要风险或取舍:缺失 publication 的响应从通用 500 改为稳定的 404;认证、tenant/workspace/session 查询条件及已有 publication 的验证顺序保持原样。
  • 未验证/不在范围内:真实 OSS、MySQL 集成配置、本地 macOS/Linux 执行。已有 Windows 文件系统测试失败已在上文说明。
  • 兼容性/迁移说明:没有数据库迁移或请求结构变更;请求缺失 publication 的调用方会收到新的状态码和错误码。

关联 Issue

Fixes #13563

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Qwen Triage finished — view run. See the stage comments in this thread for the result.

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

@doudouOUC

Copy link
Copy Markdown
Collaborator

独立复核确认:本 PR 修复与 #13563 一致,把未知 publication 的响应从 500 internal_error 改为 404 managed_tool_publication_unknown,覆盖范围与 PR 描述完全吻合。下面给出按源码独立走查得到的证据,便于 reviewer 直接核对。

三条路由的 404 落点

按 PR head 4f46d953 复核 ToolPublicationController.java 的路由 → ToolPublicationDataStore.java 的落点:

路由 Controller DataStore 入口 命中 404 的位置
GET .../finished ToolPublicationController.java:153-159 (finished → data.finished) finishedInternal ToolPublicationDataStore.java:602-605(jdbc.queryForList + requireContract(!publications.isEmpty(), NOT_FOUND, "managed_tool_publication_unknown", ...))
POST .../admissions/prepare ToolPublicationController.java:161-171 (admission → data.prepareAdmission) prepareAdmission 先在 :643 调 finished(key, publicationId, writerToken) prepareAdmission:643 的前置 finished() 会先走到 finishedInternal:602 的 404——unknown publication 在进入 :659 的 queryForMap 前就已抛 404,transactions.execute 内的写路径不可达
POST .../range ToolPublicationController.java:199-219 (range → data.readRange) readRange:1605 → readRangeInternal → verifiedStream ToolPublicationDataStore.java:1698-1701(同样的 queryForList + requireContract(... NOT_FOUND ...))

三条路由都收敛到同一对 requireContract(..., HttpStatus.NOT_FOUND, "managed_tool_publication_unknown", "Publication is unknown"),错误码与 PR 描述一致。

writer 认证仍在 publication 查询之前

readRange:1606-1607 先调 sessions.restore(..., writerToken),再到 verifiedStream 的 publication 查询;prepareAdmission 入口的 finished(..., writerToken) 内部同样先走 writer 校验路径——403 writer_credential_invalid 仍会先于 404 返回,与 PR 表格里"无效凭据 → 403"的断言一致。requireContract 在 main 上已存在(ToolPublicationDataStore.java 私有静态方法,抛 ApiException(status, code, message)),ApiExceptionHandler.api(ApiExceptionHandler.java @ExceptionHandler(ApiException.class))把 ApiException 映射为 {status, error.code, error.message} 响应,链路完整。

关于改动范围的一个观察(非阻塞)

ToolPublicationDataStore.java 在写/操作路径上还有 ~8 处 jdbc.queryForMap 也按 scope_key + publication_id 查 publication(L149/445/555/659/722/811/886/1028/1313/1464)。这些不是本 PR 的目标——它们位于 grant/finish/recover/claim/admission-commit 等内部操作路径,进入时 publication 通常已由 grant 流程创建,且多数前面已有 authorize(...)/finished() 等前置校验;以 beginFinish:811/installFinish:886 为例,调用方 grant/publishSegment/seal 阶段已写入该 publication 行,到达这些 queryForMap 时 publication 已存在,不会触发 EmptyResultDataAccessException。本 PR 把修复严格限定在 finished/prepare/range 三条 reader 路由、并把 6 条新 HTTP 回归用例(ToolPublicationControllerTest.returnsNotFoundForUnknownPublication + authenticatesWriterBeforeLookingUpPublication,均 @ValueSource(strings={"/finished","/admissions/prepare","/range"}))落在控制器边界——这个范围切分是合理的,不在本 PR 处理其他操作路径的 queryForMap 也是可接受的(它们属于另一类"已存在 publication 的状态机推进"而非"未知 publication 的领域错误")。如果后续要统一,建议另起 issue 跟踪操作路径上是否也需要同样的 requireContract 兜底。

测试

新增 6 条控制器级回归(ToolPublicationControllerTest.java:96-112)用真实 H2 + Flyway 迁移 + MockMvc + ApiExceptionHandler,断言 status().isNotFound() 与 jsonPath("$.error.code").value("managed_tool_publication_unknown"),并断言无效 writer 凭据仍返回 403——覆盖了 PR 表格里的四行 Before/After。setupPublicationReads() 构造了一个已认证的 writer lease 但不创建 publication,复现条件干净。

结论:修复正确、范围与描述一致、测试到位,建议合入。

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the PR! This is a re-run — the head commit has not moved since the last pass, so what changed is the evidence, not the code.

Template looks good ✓ — every required heading is present and filled in, including a complete parallel Chinese translation.

Problem: observed, not theoretical. #13563 (still open) names the three routes, quotes the actual exception (EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0) and the 500 internal_error envelopes, and gives a three-step reproduction. It now also has an executed A/B behind it: on the base arm the three new cases fail with expected:<404> but was:<500>, on the head arm they pass. This clears the bar comfortably.

Direction: aligned — and unusually well specified, since the linked issue already prescribed the fix ("follow the operationStatus precedent: 404 plus a managed_tool_publication_* code"), which is exactly what landed here. Worth naming rather than inheriting silently: this changes an HTTP status on three routes, so a writer that retries 5xx and treats 4xx as terminal behaves differently afterwards. The PR documents that under Risk & Scope, and 404 for a named-but-absent resource is the more correct contract, so I'm satisfied — recording it as a deliberate contract change, not a purely internal one.

Size: not applicable. packages/sdk-java/managed-agent-server/src/main/java/… matches none of the Stage 0 core patterns, and only one package changes. For the record: 10 production lines (8+/2−) and 104 test lines. Even if it were counted as core, a 10-line fix trips neither the refactor hard block nor the 500-line escalation.

Approach: minimal, and close to what I'd have written. Two sites — exactly the two the issue identified — reusing the existing requireContract helper and the existing ApiException envelope. No new abstraction, no drive-by edits, no formatting churn. It also declined two optional suggestions from the issue thread, and I think both declines are right: the shared read helper (two call sites don't earn one), and a blanket EmptyResultDataAccessException handler, which would only ever mask genuine data-integrity errors.

Risk: no elevated risk signals — I re-ran the high-risk path check against both changed files and neither matches.

What changed since the last pass: the fork's held workflow runs were released. SDK Java and Qwen Code CI have now both completed success on this exact commit, and all 16 real checks are green. The single reason I deferred last time — "has anyone watched this run?" — now has an answer, and it is in the review below.

Moving on to code review. 🔍

中文说明

感谢贡献!这是一次重跑 —— head commit 与上一轮相同,变化的是证据,不是代码。

模板完整 ✓ —— 所有必需小节都已填写,并附有完整对应的中文翻译。

问题: 是已观测到的 bug,不是理论性加固。#13563(仍处于 open 状态)点名了三条路由,引用了真实异常(EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0)和 500 internal_error 响应体,并给出了三步复现。现在还有已执行的 A/B 佐证:base 臂上三个新用例以 expected:<404> but was:<500> 失败,head 臂上通过。证据充分。

方向: 对齐 —— 而且方向非常明确:关联 issue 已经给出修复方案(沿用 operationStatus 的先例:404 加 managed_tool_publication_* 错误码),本 PR 正是这么做的。有一点值得明确记录而非默默继承:这改变了三条路由的 HTTP 状态码,因此「对 5xx 重试、把 4xx 当作终态」的 writer 在修复后行为会不同。PR 已在 Risk & Scope 中说明,而对「指名但不存在的资源」返回 404 本就是更正确的契约,所以我认可 —— 只是记录这是一次有意的契约变更,而非纯内部改动。

规模: 不适用。packages/sdk-java/managed-agent-server/src/main/java/… 不匹配 Stage 0 的任何核心路径模式,且只涉及一个 package。记录一下:生产代码 10 行(8+/2−),测试 104 行。即便按核心路径计算,10 行的 fix 既不触发 refactor 硬阻断,也不触发 500 行升级。

方案: 足够精简,和我自己会写的方案基本一致。只改两处 —— 正是 issue 指出的两处 —— 复用已有的 requireContract helper 和已有的 ApiException 响应结构。没有新增抽象,没有顺手重构,没有格式噪音。它还谢绝了 issue 讨论中两条可选建议,我认为两个谢绝都是对的:共享读取 helper(两个调用点不值得抽一个),以及兜底的 EmptyResultDataAccessException 处理器 —— 后者只会掩盖真实的数据完整性错误。

风险: 无升级风险信号 —— 我对两个改动文件重新跑了高风险路径检查,均不匹配。

与上一轮的差别: 该 fork 被挂起的 workflow 已放行。SDK Java 与 Qwen Code CI 现在都在这个 commit 上以 success 完成,全部 16 项真实检查均为绿。上一轮我转交的唯一原因 ——「有人真的看它跑过吗?」—— 现在有了答案,详见下方审查。

进入代码审查 🔍

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4f46d953f647b55e14166055e79d0a03542b4140 · re-run with @qwen-code /triage

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal first. From the title, the "Why it's needed" section and #13563's diagnosis — before weighing the diff: swap the two unguarded queryForMap reads for queryForList plus an explicit empty check throwing ApiException(NOT_FOUND, "managed_tool_publication_unknown", …), reusing the helper that already exists rather than adding one, and pin it with per-route MockMvc tests in the existing api/ToolPublicationControllerTest.java. That is what this PR does. It matches my proposal rather than exceeding it, and I found no simpler path it missed.

No critical blockers, and no AGENTS.md violations. What follows is what I verified myself this pass rather than inherited from the last one.

The one real semantic risk in the diff, and why it's safe. queryForMap throws on both zero rows and more-than-one row; queryForList + getFirst() only handles the zero case, so on paper this trades a loud failure for a silent first-row pick. It doesn't: V20__managed_tool_publication.sql:23 declares PRIMARY KEY (scope_key, publication_id), and both rewritten queries filter on exactly those two columns (plus tenant/workspace/session). At most one row can match, so getFirst() after a non-empty check cannot mask a duplicate. No invariant is lost.

Every consumer of the two changed methods, named. finishedInternal has three callers, not the two the route list implies, so I checked all three:

  • finished(key, publicationId, writerToken) — runs sessions.restore(...) first, then the lookup. This is the GET …/finished route and the path prepareAdmission enters through, so both documented routes really do authenticate before the new 404 can fire.
  • finishedForBroker(execution) — broker-only, and it does its own queryForList first with if (rows.isEmpty()) return null;. It therefore never reaches the new check with an absent row, so broker behavior is unchanged. (The only way through is a delete racing between the two SELECTs, where the outcome changes from one unchecked exception to another — not a behavioral regression.)
  • finish(...) — calls it only after beginFinish has claimed the row, so the publication necessarily exists and the new 404 is unreachable there.

verifiedStream has two callers: readRangeInternal (reached from the public readRange, which also restores the writer session first → POST …/range), and openReferencedStream, which calls requireReferenced(...) before verifiedStream and answers 400 invalid_request for an unknown publication — so the new 404 is unreachable on that path too.

One non-blocking observation, and it is pre-existing. That last point leaves the referenced-stream path answering 400 for an unknown publication while the three reader routes now answer 404. This PR did not create the inconsistency and fixing it would widen the diff, so I am not asking for a change — but if contract consistency across publication reads matters, it deserves its own issue.

The new error code is not a published-contract change. managed-agent-public-api.openapi.json contains zero managed-tool-publications paths and zero managed_tool_publication_* codes — these are internal routes. So no spec update is required and the OpenAPI contract workflow is not implicated.

Reuse: requireContract(boolean, HttpStatus, String, String) already existed (it throws ApiException), and HttpStatus was already imported in the production file. The diff adds no import and no helper — it consumes what was there, which is the right call.

One non-blocking nit in the test. Each of the six parameterized invocations builds a fresh DB_CLOSE_DELAY=-1 H2 instance that is never closed, so the class leaks six in-memory databases per run. That matches the existing house pattern (WorkspaceCsiReservationStoreTest and friends do the same), so I'm flagging it as a conscious choice rather than an oversight, not asking for a change.

Testing evidence

This comment carries CI check results fetched through the API — and unlike last time, they are real and green on the reviewed commit. I did not build, run, or test anything from this PR; triage never executes code from the tree under review. The fork's held runs were released, so SDK Java and Qwen Code CI both completed success at 4f46d95, and all 16 real checks are green (table below).

The load-bearing part is not "CI is green", it is which job ran these tests. I read the logs rather than trusting the check names:

  • The matrix rows (ubuntu-latest / Java 11|17|21, macos-latest / Java 21, windows-latest / Java 21) run mvn clean test in qwencode and runtime-broker only — 736 tests, all com.alibaba.qwen.code.runtimebroker.*. They never touch managed-agent-server. Worth saying out loud, because it is easy to read those green rows as coverage of this diff when they are not.

  • The job that does cover it is Runtime Broker and Managed Agent MariaDB / Java 21, which runs mvn -Pmysql-integration … clean verify checkstyle:check inside packages/sdk-java/managed-agent-server. Its log contains:

    [INFO] Running com.alibaba.qwen.code.managedagent.api.ToolPublicationControllerTest
    [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.496 s -- in com.alibaba.qwen.code.managedagent.api.ToolPublicationControllerTest
    [INFO] Tests run: 1268, Failures: 0, Errors: 0, Skipped: 0
    [INFO] BUILD SUCCESS
    

    So the 104 new test lines compiled and the 7 cases (6 new + 1 existing) ran and passed on Linux, and the whole 1268-test module is green including checkstyle. That is stronger than either local run: the author's Windows host had 60 environmental Workspace-filesystem failures, and the macOS A/B had two timing flakes. The same job also runs scripts/check-failsafe-reports.js non-hosted …, which fails if a test class was silently skipped — so the 7 cases cannot have passed by not running.

Does the green suite actually pin the change? Yes, and this is the part CI alone cannot prove. @wenshao ran a two-arm A/B (head 4f46d953 vs base 9a02f405, PR test file overlaid onto base as the only difference) and reported: base 4 pass / 3 red — returnsNotFoundForUnknownPublication ×3 with Status expected:<404> but was:<500> — head 7/7 green, plus an independent store-level probe driving ToolPublicationDataStore directly (not the PR's test code) showing EmptyResultDataAccessException → 500 on base and ApiException 404 managed_tool_publication_unknown on head, with 403 writer_credential_invalid unchanged and still preceding the lookup on both arms. That is their evidence, not something I re-ran — but red-on-base plus green-in-CI is exactly the pair that makes a regression test load-bearing rather than decorative.

Final CI results for 4f46d95:

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Flyway migration version uniqueness ✅ success
Hosted process fault gates / MySQL 8.4 / Java 21 ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
Runtime Broker and Managed Agent MariaDB / Java 21 ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
macos-latest / Java 21 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Not verified, and why:

  • not verified by me at runtime: I never executed the code, so the 500 → 404 transition is established from the CI logs and the maintainer's A/B above, not from my own observation. Both are executed evidence rather than a claim, which is why I treat it as settled.
  • not covered anywhere: the MariaDB-backed and fault-gate profiles ran in CI, but real OSS, and the author's six Windows Workspace-filesystem failures, were not replayed by anyone. Those failures were reproduced on unmodified upstream by the author and do not appear on CI's windows-latest / Java 21 (green), so I read them as host-specific and unrelated — naming it as a judgement, not a measured fact.

The sandboxed lane I asked for last time was "release the held CI runs", and that has now happened and settled the behavioural claim. No further /verify or /tmux run is needed — /tmux never applied, since this change has no TUI surface and the author lacks write access.

Real-scenario tmux testing: N/A — this is an unattended CI-path run (GITHUB_EVENT_NAME=issue_comment), where triage never drives the product; and the change is an internal HTTP status code with no user-visible TUI surface regardless.

中文说明

代码审查

先说我自己的独立方案。 只看标题、「为什么需要」和 #13563 的根因分析(在权衡 diff 之前):把两处未加保护的 queryForMap 读取换成 queryForList 加显式判空,抛出 ApiException(NOT_FOUND, "managed_tool_publication_unknown", …),复用已有的 helper 而不是新增一个,并在已有的 api/ToolPublicationControllerTest.java 中按路由补 MockMvc 测试。这正是本 PR 所做的。它与我的方案一致(而非超越),我也没有找到它漏掉的更简路径。

没有阻断性问题,也没有违反 AGENTS.md。 以下是本轮我亲自核验、而非沿用上一轮结论的部分。

diff 中唯一真正的语义风险,以及它为何安全。 queryForMap 在「零行」和「多于一行」两种情况下都会抛异常;queryForList + getFirst() 只处理零行,所以纸面上这是把一次显式失败换成了默默取第一行。实际不会:V20__managed_tool_publication.sql:23 声明了 PRIMARY KEY (scope_key, publication_id),而两处被改写的查询正好以这两列为过滤条件(外加 tenant/workspace/session)。最多只会匹配一行,因此判空之后的 getFirst() 不可能掩盖重复行。没有丢失任何不变量。

两个被改方法的全部调用方,逐一点名。 finishedInternal 有三个调用方(比路由清单暗示的多一个),我三个都查了:

  • finished(key, publicationId, writerToken) —— 先执行 sessions.restore(...) 再查询。这就是 GET …/finished 路由,也是 prepareAdmission 的入口,所以两条路由确实都在新的 404 之前完成鉴权。
  • finishedForBroker(execution) —— 仅供 broker 使用,它自己先做一次 queryForList 并 if (rows.isEmpty()) return null;。因此它绝不会带着「行不存在」走到新检查,broker 行为未变。(唯一可能是两次 SELECT 之间发生删除竞态,那时结果只是从一个未检查异常变成另一个 —— 不是行为回归。)
  • finish(...) —— 只在 beginFinish 已认领该行之后调用,此时 publication 必然存在,新的 404 在该路径不可达。

verifiedStream 有两个调用方:readRangeInternal(由公开的 readRange 进入,同样先恢复 writer 会话 → POST …/range),以及 openReferencedStream —— 后者在 verifiedStream 之前调用 requireReferenced(...),对未知 publication 返回 400 invalid_request,所以新的 404 在这条路径同样不可达。

一条不阻断的观察,且属于既有问题。 上面这点意味着:referenced-stream 路径对未知 publication 仍返回 400,而三条读取路由现在返回 404。这个不一致不是本 PR 造成的,修它还会扩大 diff,所以我不要求改动 —— 但如果在意 publication 读取接口的契约一致性,值得单独开一个 issue。

新错误码不属于已发布契约的变更。 managed-agent-public-api.openapi.json 中 managed-tool-publications 路径为 0 处、managed_tool_publication_* 错误码为 0 处 —— 这些是内部路由。因此无需更新 spec,OpenAPI 契约 workflow 也不受影响。

复用情况: requireContract(boolean, HttpStatus, String, String) 本来就已存在(抛 ApiException),且生产文件里 HttpStatus 已经 import。diff 没有新增 import、没有新增 helper —— 它用的是现成的东西,这个取舍是对的。

测试中一条不阻断的小问题。 六次参数化调用各自创建一个 DB_CLOSE_DELAY=-1 的新 H2 实例且从不关闭,因此该类每轮运行会泄漏六个内存数据库。这与仓库现有写法一致(WorkspaceCsiReservationStoreTest 等也是如此),所以我只是把它标为「有意识的选择」而非疏漏,并不要求修改。

测试证据

本评论携带的是通过 API 取回的 CI 检查结果 —— 与上次不同,这次是真实存在且在受审 commit 上全绿的。 我没有构建、运行或测试本 PR 的任何内容;triage 从不执行被审查代码树中的代码。该 fork 被挂起的运行已放行,因此 SDK Java 与 Qwen Code CI 都在 4f46d95 上以 success 完成,全部 16 项真实检查均为绿(见下表)。

关键不在于「CI 是绿的」,而在于是哪个 job 跑了这些测试。我读了日志,而不是只信检查名:

  • 矩阵行(ubuntu-latest / Java 11|17|21、macos-latest / Java 21、windows-latest / Java 21)只在 qwencode 和 runtime-broker 里执行 mvn clean test —— 736 个测试,全部是 com.alibaba.qwen.code.runtimebroker.*,完全不涉及 managed-agent-server。这点必须说清楚,因为很容易把这些绿行误读成对本 diff 的覆盖。

  • 真正覆盖本改动的是 Runtime Broker and Managed Agent MariaDB / Java 21,它在 packages/sdk-java/managed-agent-server 目录下执行 mvn -Pmysql-integration … clean verify checkstyle:check。其日志中包含:

    [INFO] Running com.alibaba.qwen.code.managedagent.api.ToolPublicationControllerTest
    [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.496 s -- in com.alibaba.qwen.code.managedagent.api.ToolPublicationControllerTest
    [INFO] Tests run: 1268, Failures: 0, Errors: 0, Skipped: 0
    [INFO] BUILD SUCCESS
    

    也就是说,这 104 行新测试确实在 Linux 上完成编译,7 个用例(6 新 + 1 旧)确实执行并通过,而且整个模块 1268 个测试连同 checkstyle 全绿。这比两次本地运行都更强:作者的 Windows 环境有 60 个环境性 Workspace 文件系统失败,macOS 的 A/B 有两个计时 flake。同一个 job 还会执行 scripts/check-failsafe-reports.js non-hosted …,若有测试类被静默跳过就会失败 —— 所以这 7 个用例不可能靠「没跑」而通过。

这套绿色测试真的钉住了改动吗? 是的,而这恰恰是单看 CI 无法证明的部分。@wenshao 做了双臂 A/B(head 4f46d953 对 base 9a02f405,把 PR 的测试文件覆盖到 base 臂作为唯一差异),其报告为:base 臂 4 绿 / 3 红 —— returnsNotFoundForUnknownPublication ×3,Status expected:<404> but was:<500> —— head 臂 7/7 全绿;另有一个不复用 PR 测试代码、直驱 ToolPublicationDataStore 的独立探针,显示 base 上为 EmptyResultDataAccessException → 500,head 上为 ApiException 404 managed_tool_publication_unknown,而 403 writer_credential_invalid 在两臂上均未变且仍先于 publication 查找。这是他的证据,不是我复跑的结果 —— 但「base 上红」加上「CI 上绿」正是让回归测试承重、而非装饰性的那一对证据。

上方表格给出了各检查项的真实名称与结论。未验证项及原因:

  • 未由我在运行时验证: 我从未执行代码,所以 500 → 404 的转变是由 CI 日志和上述维护者 A/B 确立的,不是我亲自观测的。两者都是已执行的证据而非口头声明,因此我视为已定论。
  • 无任何一方覆盖: MariaDB 与 fault-gate profile 在 CI 中跑过了,但真实 OSS、以及作者那六个 Windows Workspace 文件系统失败,没有人复现过。这些失败已被作者在未修改的上游复现,且 CI 的 windows-latest / Java 21 是绿的,所以我判断它们与本机环境相关且与本 PR 无关 —— 这是一个判断,不是实测结论。

上一轮我要求的沙箱通道是「放行被挂起的 CI 运行」,这件事已经发生并定论了那个行为性论断。不需要再跑 /verify 或 /tmux —— /tmux 本来就不适用,因为本改动没有 TUI 界面,且作者没有写权限。

真实场景 tmux 测试:N/A —— 本次为无人值守的 CI 路径运行(GITHUB_EVENT_NAME=issue_comment),triage 从不驱动产品;且无论如何,本改动是内部 HTTP 状态码,没有用户可见的 TUI 界面。

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4f46d953f647b55e14166055e79d0a03542b4140 · re-run with @qwen-code /triage

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the ten production lines are correct, minimal, and now backed by executed evidence on both sides (green CI at head, red base arm in the maintainer's A/B); the missing point is for two named non-blocking nits, not for any doubt about the fix.

Stepping back, this is what a good narrow fix looks like. Two sites, exactly the two #13563 identified, reusing the helper and the error envelope that already exist instead of adding parallel machinery. The author also made two judgement calls I'd have made — declining the optional shared read helper, and declining the blanket EmptyResultDataAccessException handler. The second is the more interesting, because a defensive catch-all feels safer while actually masking real data-integrity failures.

Back to my independent proposal: the PR matches it. I found no simpler path it missed, and it is not trying too hard — if anything the test harness is the heaviest part, and it is heavy because it wires real stores over a real migrated H2 instead of mocking the thing under test, which is the right trade. Every change in the diff is necessary for the stated goal; there is nothing to split out and no unrelated edits rode along. In six months I'd thank the author for this rather than curse them.

Why this pass lands differently from the last one. I deferred at 3/5 last time for one reason only: nobody had watched the code run. The fork's CI was held at action_required, so 104 new test lines had never met a compiler outside the author's Windows host, and I was not willing to be the only thing standing between never-compiled test code and main. That gap is now closed with evidence rather than assurance — and I checked which job produced it, because the green Java matrix rows do not run managed-agent-server at all. The job that does (Runtime Broker and Managed Agent MariaDB / Java 21) shows ToolPublicationControllerTest at 7/7 and the whole module at 1268 tests, 0 failures, checkstyle included, with a guard step that fails if a test class was silently skipped.

I also closed the loop the static review could not: finishedInternal has three callers, not two, and verifiedStream has two. All five are accounted for in the review above — the broker path pre-checks emptiness and returns null, the finish path only arrives after the row is claimed, and the referenced-stream path answers 400 before reaching the new check. So the reachable behavior change is exactly the three documented reader routes and nothing else. That is the "name every downstream consumer" bar, met.

The nits, neither blocking. The new test leaks six unclosed in-memory H2 databases per run, which matches the existing house pattern, so changing it here would be inconsistent rather than better. And the referenced-stream path still answers 400 invalid_request where the three reader routes now answer 404 — pre-existing, out of scope, but worth its own issue if contract consistency across publication reads matters to you.

On the vote. @wenshao's approval at this commit is a separate vote, not mine, and I did not treat it as a substitute for checking the work — I re-derived the consumer list, the primary-key invariant, and the CI coverage independently, and read the job logs rather than the check names. main requires one approving review plus code-owner review and defines no required status checks, so an approval here is load-bearing; that is precisely why I wanted executed evidence before giving one. Nothing is pending on this commit (both pull_request runs are complete), and the fork-refactor guardrail does not apply — this is a fix.

Approving, pinned to the reviewed commit. ✅

中文说明

信心度:4/5 —— 那 10 行生产代码是正确的、精简的,并且现在两侧都有已执行的证据支撑(head 上 CI 全绿,维护者 A/B 中 base 臂为红);扣掉的一分是给两条已点名的不阻断小问题,而不是对修复本身有任何怀疑。

退一步看,这是一个范围收得很好的修复该有的样子。两处改动,正是 #13563 指出的两处,复用了已有的 helper 和错误响应结构,而没有另起一套。作者还做了两个我同样会做的判断 —— 谢绝可选的共享读取 helper,以及谢绝兜底的 EmptyResultDataAccessException 处理器。第二个更有意思,因为兜底的防御性处理感觉上更安全,实际却会掩盖真实的数据完整性故障。

回到我的独立方案:本 PR 与之一致。我没有找到它漏掉的更简路径,它也没有过度设计 —— 真要说重,重的是测试脚手架,而它之所以重,是因为它接了真实的 store 和真实迁移后的 H2,而不是把被测对象 mock 掉,这个取舍是对的。diff 中每一处改动都是达成目标所必需的;没有可拆出去的东西,也没有夹带无关修改。半年后接手,我会感谢作者而不是抱怨。

这一轮为何与上一轮结论不同。 上一轮我给 3/5 并转交,只有一个原因:没有人看这段代码跑过。该 fork 的 CI 停在 action_required,所以这 104 行新测试除了在作者的 Windows 主机上之外没有见过编译器,而我不愿意让自己成为「从未编译过的测试代码」与 main 之间唯一的屏障。这个缺口现在是被证据、而不是被口头保证填上的 —— 而且我核实了是哪个 job 产出的证据,因为那些绿色的 Java 矩阵行根本不运行 managed-agent-server。真正运行它的 job(Runtime Broker and Managed Agent MariaDB / Java 21)显示 ToolPublicationControllerTest 7/7 通过、整个模块 1268 个测试 0 失败(含 checkstyle),并且带有一个「测试类被静默跳过就失败」的守护步骤。

我也补上了静态审查无法闭环的部分:finishedInternal 有三个调用方而不是两个,verifiedStream 有两个。这五处都已在上方审查中交代清楚 —— broker 路径先判空并返回 null,finish 路径只在行已被认领之后到达,referenced-stream 路径在触及新检查之前就已返回 400。因此可达的行为变化恰好是三条已记录的读取路由,别无其他。「点名每一个下游调用方」这条标准,达到了。

两条小问题,都不阻断。 新测试每轮运行会泄漏六个未关闭的内存 H2 数据库,但这与仓库现有写法一致,所以在这里改反而是不一致。另外 referenced-stream 路径对未知 publication 仍返回 400 invalid_request,而三条读取路由现在返回 404 —— 这是既有问题、超出本 PR 范围,但如果你在意 publication 读取接口的契约一致性,值得单独开 issue。

关于这一票。 @wenshao 在这个 commit 上的批准是另一票,不是我的,我也没有拿它当作免于核查的替代 —— 我独立重新推导了调用方清单、主键不变量和 CI 覆盖范围,并且读的是 job 日志而不是检查名称。main 要求一个批准 review 加 code owner 审查,且没有任何 required status check,所以这里的批准是有决定性的;这也正是我在给出批准之前一定要看到已执行证据的原因。这个 commit 上没有任何待完成的检查(两个 pull_request 运行均已完成),fork refactor 护栏也不适用 —— 这是一个 fix。

已批准,并绑定到受审 commit。✅

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4f46d953f647b55e14166055e79d0a03542b4140 · re-run with @qwen-code /triage

@wenshao

wenshao commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Local A/B verification — merge-ready ✅

Verified locally on macOS 15 (Intel), JDK 21, Maven 3.9.9, using a two-arm worktree A/B: detached trees at head 4f46d953 and base 9a02f405 (current origin/main tip), with the PR's test file overlaid onto base as the only difference, so one identical suite runs against both code versions.

A/B matrix

Core result — the regression tests are load-bearing

Gate Base 9a02f405 + test overlay Head 4f46d953
ToolPublicationControllerTest (7 tests: 6 new + 1 existing) 4 pass / 3 red — returnsNotFoundForUnknownPublication ×3, Status expected:<404> but was:<500> 7/7 green
Store probe: finished(valid writer, unknown publication) EmptyResultDataAccessException → HTTP 500 ApiException 404 managed_tool_publication_unknown
Store probe: readRange(valid writer, unknown publication) EmptyResultDataAccessException → HTTP 500 ApiException 404 managed_tool_publication_unknown
Invalid writer token, both routes (suite + probe) 403 writer_credential_invalid 403 writer_credential_invalid — unchanged, auth still precedes lookup
Full managed-agent-server suite (1268 tests) 3F+2E — the 3F are exactly the overlaid PR tests 1F+1E (both environmental, see below)
checkstyle:check on managed-agent-server n/a (code unchanged vs main) BUILD SUCCESS
Trial merge into current origin/main clean, no conflicts (true merge-base ad6039aa) —

Head, focused suite green:

head focused green

Base, same suite red on exactly the three new 404 cases:

base focused red

Independent oracle (not the PR's own test code)

I compiled a small probe against each arm's target/classes that drives ToolPublicationDataStore directly — real migrated H2 (Flyway), real ManagedSessionStore + WriterCredentialPolicy, inert dynamic proxies only for the two broker repositories and the object store. It confirms the mechanism precisely: on base, a missing publication surfaces as Spring's EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0, which the generic Exception handler maps to 500 internal_error; on head it becomes ApiException 404 managed_tool_publication_unknown. Writer authentication still throws 403 writer_credential_invalid before any publication lookup on both arms.

head probe

base probe

Full-suite failure attribution (environmental, not PR-introduced)

Neither arm is fully green on this host; the residuals are timing flakes that flap independently of the code version:

  • HarnessCoordinatorTest.runningOwnerObservesCancellationAfterStreamingStarts (mockito timeout(2_000) concurrency test): failed on head in the full run and isolated, passed on base full — then passed on head and failed on base when run alone per arm. Flaps both ways.
  • ToolPublicationStoreTest.streamsLargeOutputAndReadsItsTailAfterStoreReplacement and renewsTheOriginalClaimWhileScanningSlowObjectBytes (1 s / 500 ms operation-deadline assertions): error identically on both arms, in the full suite and isolated. Both are untouched by this PR; CI's macos-latest / Java 21 job passes them.
  • Head-only failing families after per-arm isolation: 0.

Methodology / disclosures

  • Base arm is 9a02f405 (main tip), not the author's stated ad6039aa; the two differ only by the web-shell fullscreen-table commit (feat(web-shell): Add fullscreen viewing for history tables #13589), zero Java impact — verified by diff. The trial merge above used the true merge-base ad6039aa (= head's parent).
  • ~/.m2 sibling artifacts (qwencode-sdk, qwen-managed-runtime-broker) were deleted and re-installed from each arm before that arm's runs to prevent cross-tree snapshot contamination; note the two sibling modules are byte-identical base↔head (verified by git diff --quiet).
  • runtime-broker's SpotBugs plugin requires Maven ≥ 3.8.9, so Maven 3.9.9 (wrapper dist) was used for all runs; JDK 21 for all compiles/tests.
  • Full logs and the probe source are kept under tmp/pr13602-verify-20261007-212043/ on my machine.

Not covered locally

MariaDB-backed profile (covered by CI's Runtime Broker and Managed Agent MariaDB / Java 21, passing), the fault-gates profile (needs the bundled CLI), Windows/Linux hosts, and the Node/web-shell side (untouched by this diff). The author's Windows-host failures in six Workspace filesystem suites were not replayed here; this macOS host does not exhibit them in the module I ran.

中文摘要(点击展开)

结论:可以合并 ✅(本地 A/B 验证,macOS 15 Intel + JDK 21 + Maven 3.9.9)

  • 双臂设置:head 4f46d953 与 base 9a02f405(当前 origin/main 顶)各一个 detached worktree;PR 的测试文件覆盖到 base 臂作为唯一差异,同一套测试跑两个代码版本。
  • 回归测试承重成立:ToolPublicationControllerTest 7 个用例在 base 上 4 绿 3 红(returnsNotFoundForUnknownPublication ×3,期望 404 实际 500),在 head 上 7/7 全绿。
  • 独立探针(不复用 PR 的测试代码,直驱 ToolPublicationDataStore,真实 H2+Flyway、真实会话/写凭证存储,仅 broker 仓库与对象存储用惰性代理):base 上未公开 publication 抛 EmptyResultDataAccessException(经通用异常处理器映射为 500 internal_error);head 上为 ApiException 404 managed_tool_publication_unknown。无效写凭证在双臂上均为 403 writer_credential_invalid,且先于 publication 查找——鉴权顺序未变。
  • 全套测试归因:managed-agent-server 全量 1268 例,base 3F+2E、head 1F+1E。base 的 3F 正是覆盖进去的 PR 测试;其余失败均为本机计时 flake:HarnessCoordinatorTest 取消观察用例在双臂间双向飘(head 全量红→单跑绿;base 全量绿→单跑红),ToolPublicationStoreTest 两个 1s/500ms 截止期断言在双臂相同报错。按臂隔离重跑后 head 独有失败族 = 0。
  • 静态门:head checkstyle:check 通过;与当前 main 的试合并干净无冲突(真实 merge-base ad6039aa)。
  • 披露:base 臂取 main 顶 9a02f405 而非作者的 ad6039aa,两者仅差 web-shell 的 feat(web-shell): Add fullscreen viewing for history tables #13589(零 Java 影响,已用 diff 验证);每臂运行前删除并从该臂重装 ~/.m2 的 qwencode-sdk/qwen-managed-runtime-broker 快照以防交叉污染(两模块 base↔head 字节级一致);因 SpotBugs 插件要求 Maven ≥ 3.8.9,全程使用 Maven 3.9.9。
  • 本地未覆盖:MariaDB profile(CI 已过)、fault-gates profile、Windows/Linux 主机、Node/web-shell 侧(本 PR 未触及)。作者在 Windows 上的 6 个 Workspace 文件系统套件失败未在本机复现,本机跑该模块无此现象。

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

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": base-side bytecode measurement — I measured only the HEAD side from the pre-built packages/sdk-java/managed-agent-server/target/classes ( finishedInternal 39…; "agent reverse-audit (round 1)": the HTTP consumer of POST /publications/{publicationId}/range — an in-repo search over *.ts/*.tsx/*.js/*.py/*.go/*.md/*.java found only the route declaratio…; "agent 6c": static javac -proc:none + javap -c -p size measurement of finishedInternal / verifiedStream at head vs merge-base (a target/ dir and ~/.m2/repository …; "agent 5": I did not execute the new tests (no Maven/JVM run in the shared worktree), so "the suite is green at this commit" is reasoned from signatures and handler mappin….

中文说明

⚠️ 已从批准降级为评论:CI still running。 已审查。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":base-side bytecode measurement — I measured only the HEAD side from the pre-built packages/sdk-java/managed-agent-server/target/classes ( finishedInternal 39…;"agent reverse-audit (round 1)":the HTTP consumer of POST /publications/{publicationId}/range — an in-repo search over *.ts/*.tsx/*.js/*.py/*.go/*.md/*.java found only the route declaratio…;"agent 6c":static javac -proc:none + javap -c -p size measurement of finishedInternal / verifiedStream at head vs merge-base (a target/ dir and ~/.m2/repository …;"agent 5":I did not execute the new tests (no Maven/JVM run in the shared worktree), so "the suite is green at this commit" is reasoned from signatures and handler mappin…。

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

@wenshao

wenshao commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge October 11, 2026 15:46
@qwen-code-review-bot

qwen-code-review-bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 34 passed · 0 failed · 34 total

Flakiness gate: not applicable — no runnable changed test files (0 out-of-scope file(s) noted in the log)

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。

脚本断言:34 通过 · 0 失败 · 34 总计

抖动门:不适用 — no runnable changed test files (0 out-of-scope file(s) noted in the log)

Verification report

PR 13602 deep verification — fix(managed-agent): return 404 for unknown tool publications

Verdict: findings — 34/34 scripted assertions passed (pass=34 fail=0); the central claim is load-bearing and the full-module gate is green. One concrete problem worth a reviewer's attention: the changed shared read has a fourth HTTP consumer the description does not name (artifact download), where a missing publication row now returns 404 managed_tool_publication_unknown instead of the deliberately-retryable 503 artifact_unavailable. Not a regression in the central claim; a scope/contract decision for the maintainer.

Verified head: 4f46d953f647b55e14166055e79d0a03542b4140 (HEAD^2)
Base (control) arm: 6936c77e60da8897c71bb9700ae57b7021e7db78 (HEAD^1, the merge-ref parent)
Merge commit measured: 39fd278c229f9ad37bcb495a8e4026462616685f
Diff: 2 files, +112/−2 — one production file (ToolPublicationDataStore.java, 2 hunks) + one test file.

中文摘要

结论:findings(有发现,非阻塞)。34 条脚本化断言全部通过(pass=34 fail=0)。

A/B 结论(见「Central claim + A/B」表与 01-ab-base-500-vs-head-404.png):把 PR 自带的测试文件原样拷到未修改的 base 树(HEAD^1)上运行,7 个用例中 3 个失败,失败信息正是 Status expected:<404> but was:<500>;在 head 上 7/7 全绿。3/3 从「坏」翻转为「修好」,另外 4 个无关用例两侧一致。改动是 load-bearing 的。

变异矩阵(见 03-mutation-matrix-marker-verified.png):6 个变异体全部被杀死,0 个存活。每个变异体都先用 javap 在编译产物的字节码里验证过标记确实落地,再计数。两个正向对照(把 NOT_FOUND 改成 CONFLICT、把错误码字符串改掉)都如期变红,证明断言本身是活的。关键结论:两个 hunk 的作用范围互不重叠 —— 只回滚站点 1 恰好让 /finished 与 /admissions/prepare 变红,只回滚站点 2 恰好让 /range 变红。同时证明 /admissions/prepare 的 404 来自站点 1,而不是 requireStagedCall(后者在无事务时直接 return)。删除 writer 认证会让 3 个「先认证后查询」用例全红,说明该顺序测试非空洞。

同类问题扫描(见 02-route-sweep-sibling-base-vs-head.png):用同一个 writer 凭据驱动全部 12 条 publication 路由、传入不存在的 publication id。head 上没有任何一条路由再返回 500;base 上恰好 3 条返回 500,就是本 PR 修的 3 条。其余 9 条路由在两侧逐字节一致 —— 零附带改动。也就是说这一类 bug 在 writer 面上已被完整关闭。

发现(1 条,非阻塞):verifiedStream 这个共享读取点还有第四个消费者 —— 制品下载路径 ManagedArtifactService → ManagedArtifactReader.openReferencedStream。它的 catch 块里有一份显式白名单,专门把 publication 侧的两个错误码翻译成 503 artifact_unavailable(可重试)。新增的 managed_tool_publication_unknown 不在白名单里,于是原样抛出:制品下载在 publication 行缺失时由 503(可重试)变成 404(永久),并且把 managed_tool_publication_* 命名空间的错误码泄漏到了 artifact_* 路由上。PR 描述称改动只影响三条路由,这一条未被提及。此项由代码推导得出,未端到端驱动(构造 REFERENCED 制品再删除 publication 行的夹具成本超出预算)。

未覆盖范围:MySQL/MariaDB failsafe 集成档(本 lane 只有 JDK 21 + H2,无数据库)、真实 OSS、Java 11/17 矩阵、上述制品路径的端到端驱动、跨 scope(publication 属于别的 session)的实测、broker 专用路径 finishedForBroker 的实测。详见 Not covered。

Central claim + A/B

Central claim. An authenticated writer requesting an unknown tool publication receives 404 managed_tool_publication_unknown from GET …/finished, POST …/admissions/prepare, and POST …/range, where base returned 500 internal_error; writer authentication still runs before the publication lookup.

Secondary claims. (1) The refusal is a domain error, not a swallowed server exception. (2) Nothing else on the publication surface changes.

A/B cells — the PR's own test file, run unchanged on both arms

The instrument is the PR's new ToolPublicationControllerTest (7 cases). The control arm is a scratch worktree at HEAD^1 with only the test file copied in; production code is unmodified base.

Cell Environment Observable oracle Result
HEAD (PR) merge tree 39fd278, H2 + Flyway (63 migrations), real ManagedSessionStore / WriterCredentialPolicy / ToolPublicationStore / ToolPublicationDataStore / ToolPublicationAdmissionStore, real ApiExceptionHandler, MockMvc surefire counts + per-case status 7 run, 0 failures, 0 errors ✅
BASE (control) git worktree at HEAD^1 6936c77, identical harness, PR test file copied in verbatim (diff against head: identical) surefire counts + assertion text 7 run, 3 failures ✅ (expected red — the control arm must fail)

Failing assertion on the control arm, verbatim:

[ERROR]   ToolPublicationControllerTest.returnsNotFoundForUnknownPublication:55 Status expected:<404> but was:<500>

This is the intended assertion (expected-vs-actual status values), not a compile/import/fixture break — so the revert reaches the behaviour the test exists to catch.

Per-route flip (index → route, from the test's @ValueSource):

# Route BASE HEAD Δ
[1] GET /publications/{id}/finished 500 internal_error 404 managed_tool_publication_unknown fixed
[2] POST /publications/{id}/admissions/prepare 500 internal_error 404 managed_tool_publication_unknown fixed
[3] POST /publications/{id}/range 500 internal_error 404 managed_tool_publication_unknown fixed
— writer-auth ordering × 3 403 writer_credential_invalid 403 writer_credential_invalid unchanged
— pre-existing range-coercion case pass pass unchanged

3/3 flip from broken to fixed; 4/4 unrelated cases unchanged. Witness: 01-ab-base-500-vs-head-404.png.

Scenario axes

The diff changes how a request is refused (not cancelled/parked/retried), so the relevant axes are credential validity, route, and publication existence. Settings that ran:

Axis Settings exercised Not exercised
Route all 12 publication routes (see sweep below) —
Credential valid writer token + acquired lease; invalid writer token expired lease; revoked writer; wrong generation
Publication existence nonexistent id exists-but-not-FINISHED; exists-but-quarantined; exists in another scope (inferred only, see Not covered)
Approval/tenancy single tenant/workspace/session cross-tenant id collision

Mutation matrix (marker-verified)

Every mutant was proven to reach the compiled artifact before counting: javap -c -p on ToolPublicationDataStore.class, scoped to the mutated method's bytecode, asserting the expected instruction/constant is present (or absent). All 6 markers read ok.

Mutant Marker in bytecode Red cases Interpretation
(unmutated baseline) — none — 7/7 green runner is live
revert-site1 (finishedInternal → queryForMap) queryForMap in finishedInternal returnsNotFound[1], [2] site 1 is load-bearing for /finished and /admissions/prepare
revert-site2 (verifiedStream → queryForMap) queryForMap in verifiedStream returnsNotFound[3] site 2 is load-bearing for /range only
revert-both both methods [1], [2], [3] reproduces the base arm exactly ✅
ctl-status-conflict (positive control) HttpStatus.CONFLICT in finishedInternal all 3 returnsNotFound the status().isNotFound() assertion is live
ctl-code-marker (positive control) mutant_marker_code in finishedInternal all 3 returnsNotFound the jsonPath("$.error.code") assertion is live
no-writer-auth (delete both sessions.restore(…)) ManagedSessionStore.restore absent from finished( and readRange( all 3 authenticatesWriterBefore… the ordering test is non-vacuous and load-bearing

6/6 killed, 0 survivors. No coverage gap, no dead code, no redundant defence: the two hunks guard a disjoint route set, so neither masks the other (this is the combination-row question, and revert-both is that row — it adds no red beyond the union of the two single-hunk rows, confirming disjointness rather than layered defence).

Two facts the matrix settles that reading cannot:

  • /admissions/prepare's 404 comes from site 1, not from ToolPublicationStore.requireStagedCall. That method returns immediately when no transaction is active (if (!TransactionSynchronizationManager.isActualTransactionActive()) return;), and prepareAdmission calls it before opening one — so it cannot be the source. revert-site1 turning [2] red is the proof.
  • The pre-existing rejectsCoercedOrOverflowedRangeNumbersBeforeReading case is untouched by every mutant — it never reaches the store.

Witness: 03-mutation-matrix-marker-verified.png.

Sibling sweep — is the bug class closed?

The mechanism here is a parser-shaped one: queryForMap on qwen_tool_publication throwing EmptyResultDataAccessException, which falls through to ApiExceptionHandler's generic @ExceptionHandler(Exception.class) → 500 internal_error + LOG.error. The three routes in the issue are three doors into that room, so I drove every publication route with an unknown id, on both arms, under one valid writer credential and lease.

Route Cred BASE HEAD Δ
GET /publications/{id}/finished writer 500 internal_error 404 managed_tool_publication_unknown changed
POST /publications/{id}/admissions/prepare writer 500 internal_error 404 managed_tool_publication_unknown changed
POST /publications/{id}/range writer 500 internal_error 404 managed_tool_publication_unknown changed
POST /publications/{id}/receipts/commit writer 400 invalid_request 400 invalid_request identical
POST /receipts/verify writer 400 invalid_request 400 invalid_request identical
GET /publications/{id}/operations/{op} pub-token 404 managed_tool_publication_operation_unknown same identical
POST /publications/{id}/operations/{op}/recover pub-token 400 invalid_request 400 invalid_request identical
POST /publications/{id}/finish pub-token 400 invalid_request 400 invalid_request identical
POST /publications/{id}/streams/stdout/seal pub-token 400 invalid_request 400 invalid_request identical
POST /publications/{id}/streams/stdout/prefix pub-token 400 invalid_request 400 invalid_request identical
POST /publications/{id}/segments/stdout/0 pub-token 400 invalid_request 400 invalid_request identical
POST /publications/{id}/resources/{kind}/{slot} pub-token 400 invalid_request 400 invalid_request identical

12 routes swept per arm; 3 changed; 9 byte-identical. Zero 500s remain at head (base had exactly 3). So the bug class is closed across the whole writer-reachable publication surface, and the change carries no collateral effect on any route the PR did not intend to touch. The other two writer routes already refused via queryForList + require(...), which is why they were never 500s.

Witness: 02-route-sweep-sibling-base-vs-head.png.

Targeted gate — full managed-agent-server module at head

Tests run: 1983, Failures: 0, Errors: 1, Skipped: 85

The single error is RuntimeBrokerDefaultOnTest.defaultCombinationBootsWithTheYmlDefaultsBound, a Spring context-load failure whose root cause is java.nio.file.NoSuchFileException: /etc/machine-id. A/A control: the same test run on the base arm errors identically (Tests run: 1, Failures: 0, Errors: 1, same root cause), and /etc/machine-id genuinely does not exist in this container (ls: cannot access '/etc/machine-id': No such file or directory). This is the sandbox, not a regression — measured, not assumed. No other test class failed or errored.

Note this is a stronger result than the PR body reports: the author measured 1268 tests: 1204 passed, 1 failure, 59 errors on a Windows host and attributed 60 failures to filesystem/mount behaviour. On Linux the same module is green apart from one container-specific missing file, which supports the author's attribution rather than contradicting it.

Checkstyle / SpotBugs

Measured at head on the module: mvn -DskipTests checkstyle:check spotbugs:check → You have 0 Checkstyle violations., BugInstance size is 0, BUILD SUCCESS, exit 0 (gate-head-static.log). This independently confirms the PR body's static-check claim.

Gate liveness

The gates above are only evidence if they can fail, so each was proven live rather than assumed green: the A/B control arm is the liveness proof for the test suite (it goes red 3/7 on base with the intended assertion); the two positive-control mutants (ctl-status-conflict, ctl-code-marker) are the liveness proof for the new assertions, each turning all three cases red; and the mutation runner requires the unmutated baseline green before any row counts. The full-module gate's one error is attributed by an A/A control on the base arm, not by inspection.

Corrections

Two statements in the PR description are inaccurate in ways that would mislead the next reader. Both are corrections to the description, not requests to change the code.

  1. "The two shared reads check for a missing record" understates the reach. verifiedStream (site 2) is not private to the range-read route — it is also entered by ManagedArtifactReader.openReferencedStream, i.e. by the artifact-download API. The description's scope statement ("from the finished-result, admission-prepare, and range-read routes") and its breaking-change note ("callers requesting a missing publication receive the new status and error code") both read as covering three routes; a fourth API family is affected. Evidence and consequence in Finding 1.

  2. "gives the caller a consistent refusal" holds for the three routes changed, but not across the writer surface. After this PR an unknown publication yields 404 managed_tool_publication_unknown on three writer routes and 400 invalid_request ("The request body is invalid.") on the other two (/receipts/commit, /receipts/verify). Measured — see the sweep table. Those two 400s are pre-existing (identical on both arms) and are not this PR's doing; the correction is only that "consistent" is scoped to three of five writer routes.

Findings

1. Medium — an unnamed fourth consumer: artifact download turns a retryable 503 into a permanent 404

Status: inferred from code, not driven end-to-end. The call chain and the handler are quoted below; building a REFERENCED artifact and then deleting its publication row was out of budget (see Not covered).

Chain to changed site 2:

ManagedArtifactService.read(...)                       // HTTP artifact download
  → reader.readRange(artifact, …) / reader.open(artifact, …)
  → ManagedArtifactReader.verified(artifact, lease, guard)          :121
  → data().openReferencedStream(source.sessionKey(), artifact.publicationId(), …)  :135
  → ToolPublicationDataStore.verifiedStream(…)          // ← changed site 2

ManagedArtifactService's handler for that block (ManagedArtifactService.java:275-287):

} catch (RuntimeException error) {
    if (response.isCommitted()) {
        throw new IOException("Artifact stream interrupted", error);
    }
    if (error instanceof ApiException api) {
        if ("tool_output_session_retired".equals(api.getCode())
                || "tool_output_read_expired".equals(api.getCode())) {
            session(tenant, sessionId);
            throw unavailable();          // 503 artifact_unavailable
        }
        throw api;                        // ← the new 404 escapes here, untranslated
    }
    throw unavailable();                  // ← base: EmptyResultDataAccessException landed here → 503
}

with unavailable() = new ApiException(SERVICE_UNAVAILABLE, "artifact_unavailable", "Artifact content is unavailable.").

BASE HEAD
exception from verifiedStream EmptyResultDataAccessException (not an ApiException) ApiException(404, managed_tool_publication_unknown)
branch taken final throw unavailable() throw api
HTTP response 503 artifact_unavailable 404 managed_tool_publication_unknown
client reading retryable — content temporarily unavailable permanent — resource does not exist

Why this deserves attention rather than a shrug:

  • The allowlist shows intent. The author of this handler explicitly enumerated the publication-side codes that must be translated back into the artifact vocabulary, and mapped them to a retryable 503. managed_tool_publication_unknown is a new member of exactly that family and is not in the list, so it bypasses a deliberate translation layer.
  • A retryable signal becomes permanent. If a publication row is transiently invisible to a reader whose artifact row still exists (retention/GC race, a mid-migration window), a client that previously retried on 503 will now treat the artifact as gone and may drop it. This is the "rank by observability" case: the loud, retryable failure became a quiet, terminal one.
  • Namespace leak across an API boundary. An artifact-download client switching on artifact_not_found / artifact_unavailable now receives a managed_tool_publication_* code on an artifact_* route.

Blast radius — exactly one surface, and I narrowed it by checking the other candidate. Artifact download is the only consumer whose behaviour changes: both its full-read and its partial/range-read branches go through ManagedArtifactReader.verified(…) (:121 → :135), and ManagedArtifactService is the only caller with an ApiException-specific translation that the new code escapes.

The other consumer of changed site 2 is the async projection path ManagedToolResultProjector.project(claim) → resolve(…) → reader.verified(artifact, lease, guard) at ManagedToolResultProjector.java:222. That path is unchanged, and I verified it rather than assuming it: its handler is

} catch (NoSuchElementException error) {
    fail(claim, "UNSUPPORTED", "public_turn_mapping_missing", error);
} catch (IllegalArgumentException error) {
    fail(claim, "QUARANTINED", "tool_result_source_invalid", error);
} catch (RuntimeException | IOException error) {
    fail(claim, "RETRYABLE", "tool_result_source_unavailable", error);
}

ApiException extends RuntimeException (ApiException.java:6), and base's EmptyResultDataAccessException is also a RuntimeException; neither is a NoSuchElementException or an IllegalArgumentException. Both therefore fall into the same third catch and produce the same RETRYABLE / tool_result_source_unavailable outcome — no behaviour change, and the retry semantics that Finding 1 loses on the HTTP path are preserved here. Note the line that looks like the call site, ManagedToolResultProjector.java:248 (reader.readRange(selected, stream, …)), is not one: it consumes the already-built VerifiedStream from the map populated at :222.

Sites 1 and 2 are reached only from these consumers plus the three writer routes already measured.

What this is not. It is not a failure of the central claim (that A/B is clean), not a new 500, and not an authentication or tenancy change — the guard runs after sessions.restore(...), and the sweep confirms the three writer routes and all nine untouched routes behave as intended. No data loss or security consequence was demonstrated.

Suggested minimal fix (preserves the commit's intent)

Add the new code to the existing translation list in ManagedArtifactService, so the artifact route keeps its own vocabulary and its retryable semantics:

if (error instanceof ApiException api) {
    if ("tool_output_session_retired".equals(api.getCode())
            || "tool_output_read_expired".equals(api.getCode())
            || "managed_tool_publication_unknown".equals(api.getCode())) {
        session(tenant, sessionId);
        throw unavailable();
    }
    throw api;
}

Not measured. Per this skill's own bar a suggested fix must be driven through the same harnesses; I did not build the artifact fixture, so I am not claiming the three usual results (hostile fixtures clean / benign fixtures byte-identical / suite counts unchanged). If the maintainer prefers to accept the 404 as more accurate instead, the actionable item reduces to naming the fourth route in the description and the breaking-change note. Either way a fixture that would pin this axis is missing: an artifact-read test whose publication row is deleted, asserting the intended status. Today the module suite is green with and without the change on this axis, so nothing would catch a regression here.

2. Suggestion — the suppressed LOG.error is correct, and the reason still reaches the caller

Checked per the "follow the value" rule, because this PR suppresses output. Base path: EmptyResultDataAccessException → generic @ExceptionHandler(Exception.class) → LOG.error("Managed Agent request failed", error) + 500. Head path: ApiException → @ExceptionHandler(ApiException.class) → no logging, but the cause is carried in the response envelope the caller reads (error.code = managed_tool_publication_unknown, error.message = "Publication is unknown").

So the information survives in a field with a reader, and the suppression is exactly what the description intends — not a finding. One residual tradeoff worth naming: for the genuinely anomalous variant (a publication row that vanished underneath a live writer lease), operators lose the server-side log line entirely and see only a client-reported 404. Acceptable for an expected missing-resource case; just not free.

3. Suggestion — pre-existing, not this PR: two writer routes answer "unknown publication" with a misleading 400

/receipts/commit refuses via require(candidates.size() == 1, "Admission candidate is missing") and /receipts/verify via require(rows.size() == 1, "Original committed publication is missing or ambiguous"). ToolPublicationContract.require throws IllegalArgumentException, which ApiExceptionHandler.invalid(...) renders as 400 invalid_request / "The request body is invalid." — but the body is fine; the publication does not exist. Each predicate also conflates "publication unknown" with "publication exists but has no admission object".

Measured identical on both arms, so pre-existing and out of this PR's scope; recorded only because it bounds correction 2 above and because it is the same bug class in a milder costume. A follow-up could map these to the same 404.

Checked and disproved (no finding)

  • queryForMap → queryForList().getFirst() does not weaken a uniqueness check. queryForMap also throws IncorrectResultSizeDataAccessException on >1 row, which getFirst() would silently swallow. That concern does not apply: qwen_tool_publication has PRIMARY KEY (scope_key, publication_id) (V20__managed_tool_publication.sql:23), and both queries filter on exactly those two columns plus narrowing tenant/workspace/session predicates. At most one row can match, so getFirst() is total after the isEmpty() guard and no duplicate-row behaviour changed.
  • No catch clause on the path is bypassed by the new exception type. The only EmptyResultDataAccessException catches in managed-agent-server and runtime-broker are ManagedAgentStore.java:3572,3592 — a different store, unrelated to publications. (The ApiException catch that does matter is Finding 1.)
  • 404 follows existing house precedent. GET …/operations/{op} already returns 404 managed_tool_publication_operation_unknown (measured in the sweep), so a scoped 404 + specific code is the established shape, not a new convention.

Not covered

  • Finding 1 was not driven end-to-end. No fixture was built for "REFERENCED artifact whose publication row is missing". The chain and handler are quoted from source at head; the 503→404 delta is inferred from code, not observed on the wire. What would close it: a ManagedArtifactReadIntegrationTest-style case that deletes the publication row and asserts the status.
  • Cross-scope existence leak — inferred only. A publication that exists under a different tenant/workspace/session takes the same empty-result branch (the WHERE clause carries tenant_id, workspace_id, session_id), so it should return the identical 404 managed_tool_publication_unknown with no existence leak. I did not plant a row in a second scope to measure it.
  • Broker-only path finishedForBroker. It also reaches changed site 1, and is not exercised by the PR's HTTP tests. Inferred unchanged: its own scope-less query finds the row, it derives the key from that row's tenant/workspace/session, so the scoped re-query in finishedInternal matches. Not measured.
  • MySQL/MariaDB failsafe integration profiles and the hosted harness — need a real database and a bundled dist/cli.js; this lane is JDK 21 + H2 only.
  • Real OSS object storage — ToolPublicationObjectStore was mocked in every harness (as it was in the PR's own).
  • Java 11/17 matrix — stays on sdk-java.yml; this lane has Temurin 21 only.
  • Node/TypeScript gates — not run. The diff touches no Node code (git diff HEAD^1..HEAD --stat: 2 Java files), so there is nothing to gate.
  • Trial merge into current main — the CI checkout already is the merge ref, and the full-module gate ran on it. The metadata snapshot's baseRefOid (9a02f405…) differs from HEAD^1 (6936c77e…), i.e. main moved between snapshot and merge; the merge ref is what lands and is what I measured. Depth-2 history cannot enumerate what main added in between, so I cannot list base-side deltas — the green 1983-test gate on the merged tree is the coverage for semantic-conflict risk.
  • Per-commit attribution — not needed and not skipped: git rev-list HEAD^1..HEAD^2 returns 1 commit, matching the metadata commits array length of 1, so the aggregate diff is the single commit.
  • Real OSS-backed large-output tail read after replacing the store instance (Reviewer Test Plan step 3, second half) — needs real object storage. The in-process half of step 3 (an existing publication can still be finished/admitted/range-read) is covered by the 1983-test module gate, which includes the publication storage and admission acceptance suites.

Reviewer Test Plan, walked step by step

Step Result
1. Valid writer credential + lease; unknown id through /finished, /admissions/prepare, /range → 404 managed_tool_publication_unknown Performed, all three. Measured in the sweep and in the PR's own parameterised test at head. ✅
2. Same three requests with an invalid writer credential → still 403 writer_credential_invalid before publication lookup Performed, all three. Green at head and at base (so the ordering is preserved, not newly introduced), and the no-writer-auth mutant turns all three red — the claim is pinned by a live assertion. ✅
3. An existing publication can still be finished, admitted, and range-read, including the tail of a large output after replacing the store instance Partially performed. The no-regression half is covered by the full-module gate rather than by a purpose-built accept-path probe of my own — measured inside it: ToolPublicationStoreTest 110/110, SurfaceAdmissionAcceptanceTest 195/195, ToolPublicationAcknowledgementTest 25/25, ToolPublicationCollectorTest 40/40, ToolPublicationRetentionStoreTest 18/18, AliyunToolPublicationReadRetryTest 15/15, all 0 failures. The 110 matches the PR body's own figure exactly; the acceptance suite reads 195 here against the body's 165, which is expected drift — the author measured on base ad6039aa, this ran on the merge tree, and main moved in between. The "replace the store instance" / large-output-tail half needs real object storage and was not run — see Not covered. ⚠️

No step was structurally impossible to perform.

Methodology

CI verify lane: node:22-bookworm container, no GitHub token, working tree at refs/pull/13602/merge (depth 2). Temurin JDK 21.0.12.1 + Maven 3.9.11, MAVEN_ARGS=-Dmaven.repo.local=/__w/_temp/verify-maven-repo; qwencode and runtime-broker were pre-installed from this checkout (java-prepare.log: qwencode=0 runtime-broker=0).

The A/B control arm is a scratch worktree (git worktree add tmp/base-tree HEAD^1). Reusing the shared Maven repository for the sibling modules is a clean control here because the PR leaves them untouched — asserted, not assumed: git diff HEAD^1..HEAD --stat -- packages/sdk-java/qwencode packages/sdk-java/runtime-broker is empty, and qwencode/runtime-broker are consumed as installed jars, not as symlinks into the head tree, so no head code can leak into the base arm.

All harnesses are mock-free with respect to the unit under test: real H2 + Flyway (63 migrations applied), real ManagedSessionStore / WriterCredentialPolicy / ToolPublicationStore / ToolPublicationDataStore / ToolPublicationAdmissionStore, real controller mappings and the real ApiExceptionHandler, driven over Spring MockMvc. Only ToolExecutionRepository, RuntimeBindingRepository and ToolPublicationObjectStore — none of which these read paths touch — are mocked. Oracles are HTTP status plus the parsed error.code from the response envelope, taken from the handler's output rather than from any store-internal signal.

The route sweep is an arm-agnostic recorder: the identical VerifyRouteSweepTest.java (harness only, not part of the PR, removed from both trees after running — git status --porcelain clean) prints one SWEEP|route|status|code line per route and asserts no statuses, so base and head outputs are directly diffable. The mutation runner (run-mutants.sh + mutate.mjs) restores the pristine source from a copy before each mutant, aborts unless every anchor matches its expected count exactly, verifies each mutant in the compiled bytecode via method-scoped javap -c -p, and restores the tree at the end (SOURCE-RESTORED-OK).

Every number in this report is produced by score.mjs from the raw logs in this directory — none is hand-counted. Raw artifacts: head-test-1.log, base-test.log, sweep-head.log, sweep-base.log, mutant-*.log (7), mutation-matrix.txt, gate-head-full-module.log, gate-base-RuntimeBrokerDefaultOnTest.log, assertions-detail.txt, assertions.json.

Flakiness gate log


verdict: n/a
summary: no runnable changed test files (0 out-of-scope file(s) noted in the log)

Evidence images

01-ab-base-500-vs-head-404

02-route-sweep-sibling-base-vs-head

03-mutation-matrix-marker-verified

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

— Qwen Code · sandboxed verification

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

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Oct 11, 2026
Merged via the queue into QwenLM:main with commit 01d3e6d Oct 11, 2026
75 of 76 checks passed
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.

fix(managed-agent): tool-publication reads of an unknown publication answer 500 to an authenticated writer

5 participants