feat(dingtalk): present tool permission requests with native interactive cards - #10457
feat(dingtalk): present tool permission requests with native interactive cards#10457BenGuanRan wants to merge 10 commits into
Conversation
Co-authored-by: Codex Using gpt-5.6-sol
acf6330 to
779699b
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action needed (PR #10457)Triaged all feedback newer than the last evaluation (2026-08-29T04:24:27Z):
No defects, suggestions, or questions were raised, so no code changes were made and no commit was created. 中文说明Autofix 评审轮次 — 无需处理(PR #10457)已对上次评估(2026-08-29T04:24:27Z)之后的所有反馈进行分类:
本轮没有提出任何缺陷、建议或问题,因此未做任何代码改动,也没有创建提交。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 1/100) — it will retry on the next scan.
See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 1/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/33463984189 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Could not produce a passing fix for this feedback (round 2/100). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own. Round stopped: the gate's packages/cli test leg fails for environmental reasons this round cannot fix in scopePR #10457 (DingTalk permission cards), same-run verification repair of the BlockerThe verification gate re-runs
All of those are inactive inside the gate. Meanwhile this 64-core host ran at
中文说明🤖 未能为该反馈产生可通过验证的修复(第 2/100 轮)。此项现在需要人工处理;循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。 本轮停止:门禁的 packages/cli 测试环节因环境原因失败,本轮无法在范围内修复PR #10457(钉钉权限卡片),针对合并提交 cf8ff569d6 被判"tests failed in 阻塞点验证门禁会以
这些照顾措施在门禁里全部失效。与此同时,这台 64 核宿主机在整个窗口期的
按设计,包测试不参与 A/B 基线对比(run-autofix-review-verification.sh: 证据
Run log: https://github.com/QwenLM/qwen-code/actions/runs/33464526848 🧠 Handled by Qwen Code · model/模型 |
…ion-cards-10388 # Conflicts: # packages/channels/base/src/ChannelBase.ts
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Autofix address-review summary — PR #10457Feedback triage
Merge: origin/main → feat/dingtalk-permission-cards-10388
Conflict 1 —
|
|
Thanks for the PR! Template looks good ✓ Problem: real and observed — linked issue #10388 (open, triaged Direction: aligned. DingTalk channel infrastructure for interactive cards already exists in this repo (status cards, question cards); this extends it to the remaining permission flow without weakening the permission boundary. No direct CHANGELOG reference in comparable agents, but permission-request UX is an actively invested area there too — nothing cuts against this. Size: cross-package change ( Approach: the scope matches the issue's acceptance criteria — a dedicated Risk: no elevated risk signals — no high-risk paths matched the revert-history analysis. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:真实且已被观测——关联 issue #10388(开放,已标记 方向:对齐。仓库中钉钉渠道已有交互卡片基础设施(状态卡片、提问卡片),本 PR 将其扩展到剩余的权限流程,且不削弱权限边界。同类产品的 CHANGELOG 中没有直接对应条目,但权限请求 UX 在那里也是持续投入的方向——没有反对此改动的信号。 规模:跨包改动( 方案:范围与 issue 的验收标准一致——钉钉包内独立的 风险:无升级风险信号——未命中 revert 历史分析中的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewNo critical blockers. The implementation tracks the existing question-card pattern closely — Three things worth a human look, none blocking:
Everything else follows convention: kebab-case modules, colocated tests, ESM imports, no drive-by changes. The 735 lines of docs are the committed design doc and implementation plans, per repo convention. Tests are substantive — no skips, no weakened assertions; the only removed assertion is the intentional second-responder semantic, replaced by its negation. sequenceDiagram
participant P1 as DingTalk user
participant P2 as DingtalkChannel
participant P3 as ChannelBase
participant P4 as PermissionCardController
participant P5 as DingTalk card API
participant P6 as Agent bridge
P3->>P4: present permission context with owner, decisions, onSettled
P4->>P5: createAndDeliver card
P1->>P5: clicks allow, deny or cancel
P5->>P2: card callback
P2->>P4: claim after owner check and decision parse
P4->>P3: respond with decision, settles at most once
P3->>P6: respondToPermission
P4->>P5: update card to approved, denied, cancelled or expired
Files changed (23 of 23 shown)
Testing evidence (PR's own CI, read via API — PR code was not executed in this run)The unit gate Not verified: real DingTalk delivery. The suite mocks the DingTalk SDK end to end, and the author states plainly that no live delivery with app credentials and a published template was run — that part of the claim rests on the author's word, and no CI here can close it.
Sandboxed verification is now in flight alongside this re-run — the 中文说明代码审查未发现阻断性问题。实现紧贴现有提问卡片模式——基础渠道中的 三处值得人工看一眼(均非阻断):
其余均符合约定:kebab-case 模块、同目录测试、ESM 导入、无顺手改动。735 行文档是按仓库约定提交的设计文档与实现计划。测试扎实——无 skip、无削弱断言;唯一被移除的断言是刻意的"第二响应者"语义变更,并已替换为其反向断言。 测试证据(读取 PR 自身 CI,本次运行未执行 PR 代码)单元门槛 未验证:真实钉钉投递。测试套件对钉钉 SDK 做了端到端 mock,作者也明确说明未使用应用凭证和已发布模板进行真实投递——该部分依赖作者陈述,本次 CI 无法补齐。 沙箱验证已随本次重跑启动—— — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — clean static review and genuinely good test coverage, but the core-module size escalation needs a maintainer's sign-off, and real DingTalk delivery was never observed. Stepping back: this is a well-built PR. My independent sketch of the feature — a presenter seam in the base channel, one idempotent settlement shared by card, text, and timeout paths, and a DingTalk controller reusing the existing card infrastructure — is essentially what landed, and the implementation mirrors the established question-card pattern closely enough that a maintainer familiar with that flow will find no surprises. The settlement-race tests are the kind that would actually catch regressions, and the security edges (foreign actors, malformed payloads, stale cards) each have a dedicated test rather than a hopeful assertion. Re-run note (same commit Two things keep this at a defer instead of an approval. First, policy: ~826 production lines spanning Not requesting changes — I found nothing that warrants them. 中文说明置信度:3/5 —— 静态审查干净、测试覆盖扎实,但核心模块规模升级需要维护者签核,且真实钉钉投递从未被观测到。 退一步看:这是一个构建良好的 PR。我对该特性的独立设想——基础渠道中的 presenter 接缝、卡片/文本/超时路径共享的唯一幂等结算、复用现有卡片基础设施的钉钉控制器——基本就是落地的方案;实现与既有提问卡片模式高度一致,熟悉该流程的维护者不会感到意外。结算竞态测试是真正能抓到回归的那种;安全边界(外部操作者、畸形载荷、过期卡片)各有专门测试,而不是侥幸断言。 重跑说明(同一提交 有两点使我选择暂缓而非批准。其一是政策:约 826 行生产代码横跨 不请求修改——我没有发现值得请求修改的问题。 — Qwen Code · qwen3.8-max Reviewed at |
|
⏸️ Deferring to @wenshao — needs a human call on this one. Why this wasn't auto-approved even though the review itself is clean:
Nothing here is a request for changes from the author — the code quality is high and the tests are substantive. What's needed is a maintainer to (a) own the sign-off on the shared-base change, and (b) arrange or perform one real DingTalk verification pass. Update (re-run on the same commit): this pass was re-triggered by @wenshao, so the defer is now addressed to you directly. Nothing in the review changed — the diff, findings, and green CI are exactly as documented above, and a sandboxed 中文说明⏸️ 转交 @wenshao 处理——这一单需要人来拍板。 审查本身干净却未自动批准的原因:
这不是对作者的修改请求——代码质量高、测试扎实。需要的只是维护者:(a)对共享基础层变更签核;(b)安排或执行一次真实钉钉验证。 更新(同一提交上的重跑):本次由 @wenshao 重新触发,暂缓说明现直接 @ 你。审查结论没有变化——diff、发现与全绿的 CI 与上文完全一致,沙箱 — Qwen Code · qwen3.8-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Unresolved, please confirm:
- [Critical] Promoted blocker from the autofix bot's issue-level comment (comment 5491209528): the review-address verification gate's packages/cli test leg reportedly fails for environmental reasons — RUNNER_NAME is absent from the gate's env -i clean-c…
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.3)
| channel.permissionPresentation = { kind: 'presented' }; | ||
| await channel.dispatchPermissionRequest(permissionRequest()); | ||
| expect( |
There was a problem hiding this comment.
[Suggestion] R1-11: Both committed plan documents landed as 'reconciled' records, but they contain five snippets that contradict the shipped implementation; a follow-up agent executing these plans task-by-task would copy code that no longer reproduces the suite. (1) This doc's Task 1 Step 1 uses harness properties that do not exist — channel.permissionPresentation / channel.presentedPermission (shipped: permissionPresentationResult / permissionPresentations, ChannelBase.test.ts:101-104) — asserts toContain on the {chatId, text} record instead of sent.at(-1)?.text, and calls a nonexistent permissionRequest() helper (shipped: emitPermission). (2) Its Task 1 Step 4 race snippet calls channel.receive('/deny'), an API that exists nowhere (shipped race test drives handleInbound(envelope(...)), ChannelBase.ts:5860, test :2025-2031). (3) Its Task 3 Step 4 wiring snippet omits the two target-match clauses shipped presentPermission enforces (interaction-presenter.ts:295-296), contradicting this plan's own Task 3 Step 3, which instructs a target-mismatch test. (4) docs/plans/2026-08-31-dingtalk-permission-card-language.md Task 2 Step 3 PERMISSION_COPY snippet uses a command: key the shipped constant does not have (ChannelBase.ts:255-284 uses request/tool/action/parameters). (5) Its Task 3 Step 3 PERMISSION_CARD_COPY snippet shows required: and two-element array terminals where the shipped constant uses title: and { description, button } objects (permission-card-controller.ts:48-72; consumers at :279-280/:348 read exactly that shape). Please reconcile all five snippets to the shipped shapes.
Witness:
not run — docs-vs-code contradictions settled by reading both sites at the reviewed commit (no run capability applies to prose snippets); e.g. plan line 79 `channel.receive('/deny')` vs shipped `handleInbound` (ChannelBase.ts:5860), and the 2026-08-31 snippet `required:` vs shipped `title:` (permission-card-controller.ts:48-72)
中文说明
两份计划文档都以「已对齐」记录的形式入库,但包含五处与最终实现矛盾的代码片段;后续按任务执行这些计划的 agent 会复制到无法复现测试套件的代码。(1) 本文档 Task 1 Step 1 使用了不存在的 harness 属性——channel.permissionPresentation / channel.presentedPermission(最终实现为 permissionPresentationResult / permissionPresentations,ChannelBase.test.ts:101-104)——对 {chatId, text} 记录而非 sent.at(-1)?.text 断言 toContain,并调用不存在的 permissionRequest() 辅助函数(最终实现为 emitPermission)。(2) 其 Task 1 Step 4 竞态片段调用 channel.receive('/deny')——该 API 在任何地方都不存在(最终竞态测试走 handleInbound(envelope(...)),ChannelBase.ts:5860,测试 :2025-2031)。(3) 其 Task 3 Step 4 接线片段缺少最终 presentPermission 强制的两个目标匹配子句(interaction-presenter.ts:295-296),与本计划自己的 Task 3 Step 3(要求编写目标不匹配测试)矛盾。(4) docs/plans/2026-08-31-dingtalk-permission-card-language.md Task 2 Step 3 的 PERMISSION_COPY 片段使用了最终常量没有的 command: 键(ChannelBase.ts:255-284 使用 request/tool/action/parameters)。(5) 其 Task 3 Step 3 的 PERMISSION_CARD_COPY 片段使用 required: 与二元数组终态,而最终常量使用 title: 与 { description, button } 对象(permission-card-controller.ts:48-72;消费者 :279-280/:348 正是读取该形态)。请将五处片段全部对齐到最终形态。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change), so no finding batch was implemented. The five plan-document snippets remain queued for reconciliation with the shipped shapes in the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动),未实施任何发现批次。五处计划文档片段与最终实现的对齐排入下一轮。
| protected async presentPermissionRequest( | ||
| context: ChannelPermissionRequestContext, | ||
| ): Promise<UserInputPresentationResult> { |
There was a problem hiding this comment.
[Suggestion] R1-27: The permission-side onSettled closure built in tryPresentPermission (ChannelBase.ts:890-903) — a duplicate of the user-input closure, not shared code — has no witness anywhere: all nine onSettled registrations in this file run on userInputPresentations contexts, and every controller/adapter test injects a hand-rolled fake. Dropping settlementListeners.add from the permission-side closure keeps the suite green, but external settlement (owner /approve text, daemon permissionResolved, run cancellation) never reaches PermissionCardController — the card stays pending and tappable until the 270 s timeout auto-denies or the next tap projects a stale state. Please add a case: presented permission, register context.onSettled(spy), settle externally through ChannelBase's real path (handleInbound owner /approve <requestId>, or emit permissionResolved), and assert the spy fires with the settlement reason.
Witness:
mutant (permission-side closure drops `settlementListeners.add`): full suite 642/642 green; probe red — `expected "spy" to be called once, but got 0 times`; intact 643/643 green
Acceptance criterion: removing pending.settlementListeners.add(listener) from the permission-side closure must turn the new test red (the existing user-input settlement tests pin the separate closure in tryPresentUserInput). Settle through ChannelBase's real path — settleUserInput guards if (pending.settled) return; (ChannelBase.ts:2946-2952) — rather than re-simulating controller internals.
中文说明
tryPresentPermission 中构建的权限侧 onSettled 闭包(ChannelBase.ts:890-903)——是用户输入闭包的复制品而非共享代码——在任何地方都没有见证:本文件全部九个 onSettled 注册都运行在 userInputPresentations 上下文上,所有控制器/适配器测试注入的都是手工伪造品。从权限侧闭包中删除 settlementListeners.add 后套件仍绿,但外部结算(发起人 /approve 文本、daemon permissionResolved、运行取消)永远到达不了 PermissionCardController——卡片保持待处理且可点击,直到 270 秒超时自动拒绝或下一次点击投影过期状态。请增加用例:展示权限后注册 context.onSettled(spy),通过 ChannelBase 真实路径外部结算(handleInbound 发起人 /approve <requestId>,或发出 permissionResolved),断言 spy 以结算原因被触发。
验收标准:从权限侧闭包移除 pending.settlementListeners.add(listener) 必须使新测试变红(现有用户输入结算测试固定的是 tryPresentUserInput 中的另一个闭包)。请通过 ChannelBase 真实路径结算——settleUserInput 有 if (pending.settled) return; 守卫(ChannelBase.ts:2946-2952)——而不要重新模拟控制器内部。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The permission-side onSettled settlement witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。权限侧 onSettled 结算见证排入下一轮。
| expect(ch.sent[0]!.text).toContain('运行工具需要授权'); | ||
| expect(ch.sent[0]!.text).toContain('操作:'); |
There was a problem hiding this comment.
[Suggestion] R1-34: The zh PERMISSION_COPY keys tool (工具:), parameters (参数:), and request (请求:) have zero witnesses — grep finds none of them in ChannelBase.test.ts — while their English counterparts are pinned (Tool: shell, Parameters: unknown, Request: req-123). Corrupting any of the three keeps the suite green, and every zh channel on the text-fallback path (every non-DingTalk channel via the base default; DingTalk with permissionCard.enabled: false) renders a mixed-language fallback message. The comparator confirms the suite can catch zh-copy corruption (corrupting action: fails the zh test) — it simply never looks at these three keys, and the probe output shows the existing fixture renders both lines. Please assert 工具: and 参数: in uses Chinese text when native permission presentation is unsupported.
Witness:
mutant (zh `request`/`tool`/`parameters` set to English): full ChannelBase.test.ts 642/642 green; comparator — corrupting zh `action` makes the zh fallback test fail, so the suite can catch zh-copy corruption but never looks at these three keys; probe output shows both lines render (`工具: run_shell_command`, `参数: command`)
Acceptance criterion: corrupting the zh tool or parameters values in PERMISSION_COPY must turn the new assertions red. The Parameters line renders only when the summary is non-empty — ...(parameters ? [...] : []) (ChannelBase.ts:3078) — so keep a rawInput that yields a non-empty permissionParameterSummary.
中文说明
zh PERMISSION_COPY 的 tool(工具:)、parameters(参数:)、request(请求:)三个键零见证——在 ChannelBase.test.ts 中 grep 均无结果——而英文对应项已被固定(Tool: shell、Parameters: unknown、Request: req-123)。破坏三者中任何一个套件仍绿,而所有走文本回退的中文渠道(基类默认下的所有非钉钉渠道;permissionCard.enabled: false 的钉钉)都会渲染中英混杂的回退消息。对照实验证实套件能捕获 zh 文案破坏(破坏 action: 会使 zh 测试失败)——只是从未检查这三个键,且探针输出显示既有 fixture 会渲染这两行。请在 uses Chinese text when native permission presentation is unsupported 中断言 工具: 与 参数:。
验收标准:破坏 PERMISSION_COPY 中 zh 的 tool 或 parameters 值必须使新断言变红。Parameters 行仅在摘要非空时渲染——...(parameters ? [...] : [])(ChannelBase.ts:3078)——因此请保留能产生非空 permissionParameterSummary 的 rawInput。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The zh tool/parameters/request copy assertions stay queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。zh tool/parameters/request 文案断言排入下一轮。
| ch.permissionPresentationHandler = async (context) => { | ||
| await context.respond('allow_always'); | ||
| return { kind: 'handled' }; | ||
| }; |
There was a problem hiding this comment.
[Suggestion] R1-26: The presentation contract's success branch — { kind: 'handled' } after a successful context.respond() (result.kind === 'presented' || (result.kind === 'handled' && respondInvoked), ChannelBase.ts:927) — has zero witnesses; the only permission handled test keeps respondInvoked false by responding with an unadvertised decision. Narrowing the condition to result.kind === 'presented' keeps the suite green, but an adapter that settles the request inside presentPermissionRequest (a fire-and-forget respond in flight) and returns { kind: 'handled' } then falls through to text dispatch and sends a /approve prompt for a request that was already approved. Please add a case whose handler fires void context.respond('allow_once') (an advertised decision, left in flight) and returns { kind: 'handled' }; assert respondToPermission is called once with the proceed_once outcome and no fallback text is sent.
Witness:
mutant (condition narrowed to `result.kind === 'presented'`): full suite 642/642 green; probe red — the `/approve … Allow once` fallback text was sent for the already-responded request; intact 643/643 green
Acceptance criterion: narrowing the condition at ChannelBase.ts:927 to result.kind === 'presented' must turn the new test red. The test must respond with an advertised decision — respondInvoked is only set when permissionPresentationResponse produced a real response (ChannelBase.ts:899-906) — and leave the respond in flight, since an awaited-and-accepted respond removes the pending record.
中文说明
展示契约的成功分支——成功 context.respond() 之后返回 { kind: 'handled' }(result.kind === 'presented' || (result.kind === 'handled' && respondInvoked),ChannelBase.ts:927)——零见证;唯一的权限 handled 测试通过以未提供的决策应答使 respondInvoked 保持 false。把条件收窄为 result.kind === 'presented' 后套件仍绿,但在 presentPermissionRequest 内部结算请求(进行中的即发即忘 respond)并返回 { kind: 'handled' } 的适配器会落入文本分发,为已批准的请求再发送 /approve 提示。请增加用例:handler 以 void context.respond('allow_once')(已提供决策、保持进行中)应答并返回 { kind: 'handled' };断言 respondToPermission 以 proceed_once 结果被调用一次且未发送回退文本。
验收标准:把 ChannelBase.ts:927 的条件收窄为 result.kind === 'presented' 必须使新测试变红。测试必须以已提供的决策应答——respondInvoked 只在 permissionPresentationResponse 产生真实应答时置位(ChannelBase.ts:899-906)——并保持应答进行中,因为被等待且接受的应答会移除待处理记录。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The handled-after-respond success-branch witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。respond 后返回 handled 的成功分支见证排入下一轮。
| it('expires and denies a timed-out permission', async () => { | ||
| vi.useFakeTimers(); | ||
| const { client, controller } = createHarness(1_000); |
There was a problem hiding this comment.
[Suggestion] R1-31: expire()'s if (record.state !== 'pending') return; guard — the only thing stopping the timeout from double-settling an already-claimed card — has no witness: both timeout tests fire only on unclaimed records, and claim() leaves the timer armed (the only clearTimeout is in removeRecord). Removing the guard keeps the suite green, but a timeout firing while the owner's accepted respond() is in flight issues a second respond('deny') and finalizes 'expired' — the approval still settles, so the card shows 'no longer available' while the tool runs. Please add a case: fake timers, createHarness(1_000), a never-resolving respond; present, claim 'allow_once', run execute, advance 1 000 ms; assert respond was called exactly once with 'allow_once' and updateInstance was not called.
Witness:
mutant (expire state guard removed): all 334 dingtalk tests stay green; probe red — `expected "spy" to be called 1 times, but got 2 times` (respond called `'allow_once'` then `'deny'`); intact green
Acceptance criterion: deleting if (record.state !== 'pending') return; from expire must turn the new test red. Do not resolve the held respond before asserting — the only clearTimeout is inside removeRecord (permission-card-controller.ts:253), so the record must stay claimed-but-not-finalized when timers advance.
中文说明
expire() 的 if (record.state !== 'pending') return; 守卫——阻止超时对已 claim 卡片二次结算的唯一屏障——没有见证:两个超时测试都只对未 claim 的记录触发,且 claim() 不解除定时器(唯一的 clearTimeout 在 removeRecord 中)。删除该守卫后套件仍绿,但当发起人已接受的 respond() 进行中时超时触发,会发出第二个 respond('deny') 并终态化为 'expired'——批准照常结算,于是工具运行时卡片显示「已失效」。请增加用例:假定时器、createHarness(1_000)、永不解析的 respond;展示、claim 'allow_once'、执行 execute、推进 1 000 毫秒;断言 respond 恰好以 'allow_once' 被调用一次且 updateInstance 未被调用。
验收标准:从 expire 删除 if (record.state !== 'pending') return; 必须使新测试变红。断言前不要解析挂起的 respond——唯一的 clearTimeout 在 removeRecord 内(permission-card-controller.ts:253),因此推进定时器时记录必须保持已 claim 未终态。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The expire state-guard witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。expire 状态守卫见证排入下一轮。
| expect(respond).toHaveBeenCalledWith('deny'); | ||
| expect(client.updateInstance).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| cardParamMap: expect.objectContaining({ card_status: 'expired' }), | ||
| }), | ||
| ); |
There was a problem hiding this comment.
[Suggestion] R1-23: No permission-card test ever rejects client.updateInstance, so the design promise that failed card updates never reopen or retry a settled permission rests solely on the try/catch in projectTerminal (permission-card-controller.ts:300-305), which has zero witnesses — while both sibling controllers test this exact seam. Removing the catch turns a card-update API failure during terminal projection into an unhandled promise rejection in the daemon worker instead of an onError log. Please add a case mirroring 'expires and denies a timed-out permission' with updateInstance mocked to reject before the terminal event; assert the controller still settles (respond with 'deny'), the record is removed (a follow-up claim returns { kind: 'ignored' }), and a harness-supplied onError spy receives 'permission card finalization'.
Witness:
mutant (try/catch removed): probe red — `expected "spy" to be called with arguments` and vitest reported `Unhandled Rejection ⎯⎯ Error: card update failed`; shipped 20-test suite green; intact probe: `respond` still `'deny'`, `onError` called with `'permission card finalization'`
Acceptance criterion: deleting the try/catch in projectTerminal must turn the new test red. Note finalize removes the record before projecting (permission-card-controller.ts:246-248), so the follow-up-claim-is-ignored assertion holds regardless of the projection failure — do not assert that a failed projection keeps the record claimable.
中文说明
没有任何权限卡片测试拒绝 client.updateInstance,因此「卡片更新失败不会重开或重试已结算权限」的设计承诺完全依赖 projectTerminal 中的 try/catch(permission-card-controller.ts:300-305),而它零见证——两个兄弟控制器都测试了这一接缝。删除该 catch 会把终态投影期间的卡片更新 API 失败变成 daemon worker 中的未处理 promise 拒绝,而不是 onError 日志。请仿照 'expires and denies a timed-out permission' 增加用例:在终态事件前把 updateInstance mock 为拒绝;断言控制器仍结算(respond 收到 'deny')、记录被移除(后续 claim 返回 { kind: 'ignored' })、harness 提供的 onError spy 收到 'permission card finalization'。
验收标准:删除 projectTerminal 中的 try/catch 必须使新测试变红。注意 finalize 在投影前移除记录(permission-card-controller.ts:246-248),因此「后续 claim 被忽略」的断言与投影失败与否无关——不要断言失败的投影会使记录保持可 claim。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The updateInstance rejection witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。updateInstance 拒绝见证排入下一轮。
| settle('resolved_outside_presenter'); | ||
| await vi.waitFor(() => | ||
| expect(client.updateInstance).toHaveBeenCalledWith({ | ||
| outTrackId, | ||
| cardParamMap: expect.objectContaining({ card_status: 'expired' }), |
There was a problem hiding this comment.
[Suggestion] R1-29: The settlement listener's non-resolved branch (reason === 'resolved_outside_presenter' ? 'expired' : 'cancelled', permission-card-controller.ts:106) is never exercised: every settle() call in this file uses 'resolved_outside_presenter', and both 'cancelled' projection tests go through cancelRun, a different code path. ChannelBase genuinely produces the other reasons — a selected reject option maps to 'cancelled' (ChannelBase.ts:2968-2987, owner answering /deny by text while a card is pending) and removePendingPermissionsForSession settles with 'run_cancelled' (ChannelBase.ts:2576, 3765, 6544). Collapsing the ternary to 'expired' keeps the suite green while the card projects 'This permission request is no longer available.' instead of 'Permission request cancelled.' Please add a case settling with 'run_cancelled' (and/or 'cancelled') while pending, asserting card_status: 'cancelled' with the cancelled copy and respond not called.
Witness:
mutant (ternary collapsed to `'expired'`): controller + presenter suites 80/80 green; probe (`settle('run_cancelled')` while pending) red — observed `card_status: "expired"` with `"This permission request is no longer available."` where cancelled expected; intact probe green
Acceptance criterion: replacing the ternary with 'expired' must turn the new test red. Settle before any claim — the listener skips claimed records (if (record.state === 'claimed') return;, permission-card-controller.ts:102).
中文说明
结算监听器的非 resolved 分支(reason === 'resolved_outside_presenter' ? 'expired' : 'cancelled',permission-card-controller.ts:106)从未被执行:本文件所有 settle() 调用都用 'resolved_outside_presenter',两个 'cancelled' 投影测试都经由 cancelRun——另一条代码路径。ChannelBase 确实会产生其他原因——选中拒绝选项映射为 'cancelled'(ChannelBase.ts:2968-2987,卡片待处理时发起人以文本 /deny 应答),removePendingPermissionsForSession 以 'run_cancelled' 结算(ChannelBase.ts:2576、3765、6544)。把三元式坍缩为 'expired' 后套件仍绿,卡片却投影 'This permission request is no longer available.' 而非 'Permission request cancelled.'。请增加待处理状态下以 'run_cancelled'(和/或 'cancelled')结算的用例,断言 card_status: 'cancelled' 与取消文案,且 respond 未被调用。
验收标准:把三元式替换为 'expired' 必须使新测试变红。请在任何 claim 之前结算——监听器会跳过已 claim 的记录(if (record.state === 'claimed') return;,permission-card-controller.ts:102)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The settlement-listener non-resolved branch witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。结算监听器非 resolved 分支见证排入下一轮。
| it('cancels every pending permission owned by a terminal run', async () => { | ||
| const { client, controller } = createHarness(); | ||
| const first = createContext('permission-1'); | ||
| const second = createContext('permission-2'); |
There was a problem hiding this comment.
[Suggestion] R1-37: The per-run scoping of cancelRun (this.pendingByRun.get(runId), permission-card-controller.ts:185) has no behavioural witness: every test in this file uses only the harness-default runId: 'run-1', including the two-card test where both cards share run-1. Widening cancelRun to all runs keeps the suite green, but in production a channel serves multiple attended sessions concurrently — one run terminalizing would finalize another run's card cancelled and remove its record, so that user's later Allow/Deny tap hits claim → ignored and the permission sits blocked until the 270 s timeout auto-denies. The sibling pins exactly this for question cards (question-card-controller.test.ts:509, 596-610). Please mirror it: present cards for permission-1 (run-1) and permission-2 with second.context.runId = 'run-2', call cancelRun('run-1'), and assert the second card receives no updateInstance and is still claimable.
Witness:
mutant (`cancelRun` widened across all runs via `flatMap((set) => [...set])`, log confirmed `requestIds: ["permission-1","permission-2"]` with runId ignored): 334 tests green; fix-witness probe red — second run's card got a cancelled projection (`expected true to be false`); restored run-scoped code green (21 passed)
Acceptance criterion: mutating cancelRun to cancel across all runs must turn the new test red. Override second.context.runId = 'run-2' — the harness hardcodes runId: 'run-1' (test :28) — and note removeRecord deletes the pendingByRun entry when its set empties (permission-card-controller.ts:262-263), so the run-2 record must remain claimable afterwards.
中文说明
cancelRun 的按运行作用域(this.pendingByRun.get(runId),permission-card-controller.ts:185)没有行为见证:本文件所有测试都只用 harness 默认的 runId: 'run-1',包括两张卡片同属 run-1 的双卡片测试。把 cancelRun 放宽到所有运行后套件仍绿,但生产中一个渠道同时服务多个人值守会话——一个运行终态化会把另一个运行的卡片终态化为 cancelled 并移除其记录,该用户随后的允许/拒绝点击会得到 claim → ignored,权限被阻塞直到 270 秒超时自动拒绝。兄弟控制器已为问题卡片固定了完全相同的行为(question-card-controller.test.ts:509、596-610)。请仿照:为 permission-1(run-1)与 second.context.runId = 'run-2' 的 permission-2 展示卡片,调用 cancelRun('run-1'),断言第二张卡片未收到 updateInstance 且仍可被 claim。
验收标准:把 cancelRun 突变为跨所有运行取消必须使新测试变红。请覆盖 second.context.runId = 'run-2'——harness 硬编码 runId: 'run-1'(测试 :28)——并注意 removeRecord 在集合清空时删除 pendingByRun 条目(permission-card-controller.ts:262-263),因此之后 run-2 记录必须仍可被 claim。
— qwen3.8-max via Qwen Code /review (v0.22.3)
There was a problem hiding this comment.
Still deferred: the current round is a same-run verification repair limited to one commit (reverting the rejected packages/cli/vitest.config.ts change). The per-run cancelRun scoping witness stays queued for the next round.
继续顺延:本轮为同轮验证修复,仅限单个提交(回退被拒的 packages/cli/vitest.config.ts 改动)。按运行作用域的 cancelRun 见证排入下一轮。
…gs (QwenLM#10457) - ChannelBase: translate the stock prefix of scoped always-allow labels but keep the command/tool scope suffix (exec/mcp confirmations always generate suffixed names, and zh channels dropped them) - packages/cli: slow-host test budgets now also key on measured host saturation (isSlowTestHost), because the review-address verification gate re-runs tests through an env -i child that does not carry RUNNER_NAME; raise the dev-entrypoint startup budget accordingly - packages/cli: llm/AppContainer/live-serve tests point HOME at a scratch directory; the gate inherits a HOME the test process cannot write to, which crashed best-effort ~/.qwen writes Adds mutation-witnessed tests for permission-card controller guards (question_id, checkbox payloads, hasBusinessPayload gate, isCancel, requestId action, per-actor forbidden dedupe) and the zh user-scope always-allow branch.
|
@qwen-code /triage |
|
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: 130 passed · 0 failed · 130 total Flakiness gate: ✅ 9 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:130 通过 · 0 失败 · 130 总计 抖动门:✅ 9 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #10457 — feat(dingtalk): present tool permission requests with native interactive cardsVerdict: 中文摘要
Central claim and A/B proofCentral claim: an attended DingTalk tool-permission request is presented as an owner-bound native interactive card whose responses — card callback, owner text command, timeout, cancel, outside settlement — share one settlement path that resolves the original request at most once and moves stale cards to terminal states. Harness: real compiled
Control hygiene: base dist imports only node builtins outside its own tree; Race-fix hunk A/B (commit
The hunk is load-bearing in both orderings: without it the loser reports success it did not cause (a wrong answer nobody is told about), while settlement count stays 1 either way — the visible defect is the fabricated confirmation, which the two-cell A/B of the whole PR could not isolate. Guard matrix (head, witness
|
| cell | result |
|---|---|
| duplicate callback after claim; stale deny after finalize | 5/5 ignored, one settlement, card stays approved |
| foreign group actor | 7/7 forbidden once, repeat silently ignored, no settlement, owner settles afterwards (denied) |
| malformed formData (extra key / two values / unadvertised decision / empty / numeric / missing field / nested non-string) + unrelated actionId + payload-less callback | 12/12 ignored, card still pending, valid claim works after |
| timeout (120 ms) | 5/5 card expired, pending denied via cancel optionId, late claim ignored |
| owner cancel action | 3/3 denied once, card cancelled |
outside settlement (permissionResolved) |
4/4 card expired, late callback ignored, bridge untouched |
card delivery failure (deliverResults success:false) |
5/5 text fallback delivered, failure surfaced to onError (not swallowed), text approval works after |
| owner-only text while card pending (per-user scope) | 3/3 non-owner text cannot settle, owner can |
owner-only text while card pending (shared chat_thread scope) |
2/2 at head; on the M6-reverted build the intruder does settle (1/1 expected-fail cell) |
Mutation matrix (witness 04-mutation-matrix.png)
Positive controls land in the same file as each mutant and are all killed, proving each runner collects the file it mutates.
| mutant | pinned suite | verdict | classification |
|---|---|---|---|
P0 zh allowOnce copy mangled (ChannelBase.ts) |
-t permission (73) |
KILLED (1 failed) | control |
| P1 en card title mangled (controller) | controller suite (20) | KILLED (1 failed) | control |
P2 'chinese' match mangled (runtime.ts) |
runtime suite (27) | KILLED (1 failed) | control |
| M1 race hunk reverted | -t "native permission response promise" |
KILLED — fails the intended assertion: expected "Permission request is no longer pending.", received "Permission denied." (log 18-M1-kill-message.log) |
vacuity check passed |
M5 text path no longer routes card-backed perms through respondToUserInput |
same filter | KILLED (1 failed) | guard pinned |
M6 sender-match clause reverted (!userInputPresented || …) |
full ChannelBase suite: 642/642 green | SURVIVED | coverage gap (see F1) |
| M2 owner binding disabled (controller) | controller suite | KILLED (1 failed) | guard pinned |
| M3 unadvertised decisions accepted (controller) | controller suite | KILLED (1 failed) | guard pinned |
| M4 state guard removed, terminal cards re-claimable (controller) | controller suite | KILLED (3 failed) | guard pinned |
M7 underscore normalization dropped (zh_CN) |
runtime suite | KILLED (1 failed) | guard pinned |
M1 and M5 defend different facets (dedup vs routing) and each is killed alone, so no layered-guard masking exists; no combination row needed.
Reviewer Test Plan, per step
- One native card instead of text — feature-head 16/16 (
sent=0, onecreateAndDeliver). - allow-once resumes once + card approved; deny once + denied — feature-head, guard-foreign (deny path), race cells (exactly-one settlement).
- Persistent choice only when advertised, original scope — feature-head (
unadvertised persistent grant omitted);allow_always → proceed_always_projectmapping pinned by the suite's "maps native permission decisions to original option ids exactly once" (also the M1 kill vehicle). - Cancel and timeout deny the pending permission and terminalize cards — guard-cancel 3/3, guard-timeout 5/5, race cells show no delayed update can race a text approval (loser gets false, one finalization).
- Other member / malformed / duplicate / stale cannot settle — guard-foreign 7/7, guard-malformed 12/12, guard-dup 5/5. Text-command side: owner-only holds at head (3/3 + 2/2) but see F1.
- Disabled or failed delivery → text instructions — fallback-delivery-failure 5/5; disabled ≡ presentation unsupported ≡ feature-base text path.
zh_CNvsen/autoat startup, no session lookup — feature-head-zh 7/7, locale probes 23/23 (05-targeted-gates.png), CLI suites pin daemon + start wiring.
Findings
F1 — Suggestion (coverage gap, non-blocking): the owner-only text-command guard is load-bearing but unpinned by any test.
canEnvelopeAnswerPendingPermission (packages/channels/base/src/ChannelBase.ts:3040) adds (!userInputPresented && !permissionPresented) || senderId match. Under per-user session scopes the pre-existing shared-target clause already enforces sender match, so reverting the new clause leaves all 642 ChannelBase tests green (M6). Under shared scopes (single, chat_thread, group thread) the new clause is the only thing stopping a group member from settling the owner's card-backed permission via /approve text: with the clause reverted my harness cell settles the permission from intruder-9 (log 17-owner-only-shared-m6.log, n=1), while head blocks it (log 16-owner-only-shared-head.log). The behavior at head is correct; the gap is that a future revert ships green. Fix is a test, not code: the harness cell owner-only-text-shared-session in perm-harness.mjs is a ready-made fixture (config sessionScope: 'chat_thread', group envelope from a second sender, assert respondToPermission not called, then owner settles).
F2 — Nice-to-have: the card carries less request detail than the text fallback it replaces.
The text fallback shows Tool:/Action:/Parameters: lines; the card's cardParamMap carries only question_desc = <title> plus the decision form (see cardData() in permission-card-controller.ts and the feature-head wire capture). An approver sees the action title but not the parameter summary the text path provided. Deliberate-looking template choice, but for a security-sensitive approval the parameter summary is the part an owner most needs; consider folding permissionParameterSummary into question_desc.
Not covered
- Real DingTalk delivery with production credentials and a published template (the PR states the same; my loopback peer encodes the documented wire semantics, i.e. this reproduces the wire shape, not DingTalk-side template rendering).
- Per-commit attribution: checkout is depth-2 (1 commit reachable vs 5 in the metadata snapshot); the aggregate
HEAD^1..HEADdiff was verified instead. - Trial merge into current
main(no network): conflict-freeness of the merge ref was pre-validated by the workflow, not re-measured here. DingtalkInteractionPresenter.presentPermissionrun/owner/target guards andDingtalkChannel.presentPermissionRequest'scardRunscheck: exercised only by the package's own suites (508/508 green); my harness drove the controller directly, which is the seam those guards protect.- Full CLI suite: only the four touched/related test files ran (27 + 129 + 15 passed).
- Live per-session language switching and native permission cards in CLI/Web/IDE/other adapters — out of the PR's stated scope.
Methodology
Environment: CI verify container (node v22.23.2), merge-ref checkout (HEAD = merge, HEAD^1 = base tip, HEAD^2 = verified head). Builds: head dist prebuilt by the workflow; base and control channel-base dists compiled with the repo's own tsc inside scratch worktrees tmp/base-tree (HEAD^1) and tmp/race-ctrl (HEAD + exactly the race hunk reverted, verified by git diff --stat = 1 file, +1/−2). Harness perm-harness.mjs drives the real compiled code; the loopback peer waits for the controller's own state machine (waitForCardPending) before injecting callbacks, closing a client/server ordering race in the harness itself (one early capture showed it; fixed and re-captured — no PR behavior involved). Mutation matrix applied per-hunk to source, ran the pinned suite, git checkout -- after each; tree confirmed clean afterwards. Raw logs in logs/, harness + drivers alongside this report. Assertion convention: each harness ASSERT line, each locale probe, each mutation verdict, and each suite run (counted once) is one scripted assertion; fail counts only unexpected outcomes (expected control/mutant behaviors are encoded as passing expectations).
Flakiness gate log
rounds=5 files=9 skipped=0
file packages/channels/base/src/ChannelBase.test.ts: (cd packages/channels/base) npx --no-install vitest run ./src/ChannelBase.test.ts
file packages/channels/dingtalk/src/DingtalkAdapter.test.ts: (cd packages/channels/dingtalk) npx --no-install vitest run ./src/DingtalkAdapter.test.ts
file packages/channels/dingtalk/src/interaction-presenter.test.ts: (cd packages/channels/dingtalk) npx --no-install vitest run ./src/interaction-presenter.test.ts
file packages/channels/dingtalk/src/interactive-card-types.test.ts: (cd packages/channels/dingtalk) npx --no-install vitest run ./src/interactive-card-types.test.ts
file packages/channels/dingtalk/src/permission-card-controller.test.ts: (cd packages/channels/dingtalk) npx --no-install vitest run ./src/permission-card-controller.test.ts
file packages/cli/src/commands/channel/daemon-worker.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/channel/daemon-worker.test.ts
file packages/cli/src/commands/channel/runtime.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/channel/runtime.test.ts
file packages/cli/src/commands/channel/start.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/channel/start.test.ts
file packages/cli/src/serve/routes/workspace-channel-management.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/workspace-channel-management.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/channels/base/src/ChannelBase.test.ts: PPPPP
packages/channels/dingtalk/src/DingtalkAdapter.test.ts: PPPPP
packages/channels/dingtalk/src/interaction-presenter.test.ts: PPPPP
packages/channels/dingtalk/src/interactive-card-types.test.ts: PPPPP
packages/channels/dingtalk/src/permission-card-controller.test.ts: PPPPP
packages/cli/src/commands/channel/daemon-worker.test.ts: PPPPP
packages/cli/src/commands/channel/runtime.test.ts: PPPPP
packages/cli/src/commands/channel/start.test.ts: PPPPP
packages/cli/src/serve/routes/workspace-channel-management.test.ts: PPPPP
verdict: pass
summary: 9 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/channels/base/src/ChannelBase.test.ts: P (exit 0)
round 1 · packages/channels/dingtalk/src/DingtalkAdapter.test.ts: P (exit 0)
round 1 · packages/channels/dingtalk/src/interaction-presenter.test.ts: P (exit 0)
round 1 · packages/channels/dingtalk/src/interactive-card-types.test.ts: P (exit 0)
round 1 · packages/channels/dingtalk/src/permission-card-controller.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/channel/daemon-worker.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/channel/runtime.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/channel/start.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/routes/workspace-channel-management.test.ts: P (exit 0)
round 2 · packages/channels/base/src/ChannelBase.test.ts: P (exit 0)
round 2 · packages/channels/dingtalk/src/DingtalkAdapter.test.ts: P (exit 0)
round 2 · packages/channels/dingtalk/src/interaction-presenter.test.ts: P (exit 0)
round 2 · packages/channels/dingtalk/src/interactive-card-types.test.ts: P (exit 0)
round 2 · packages/channels/dingtalk/src/permission-card-controller.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/channel/daemon-worker.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/channel/runtime.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/channel/start.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/routes/workspace-channel-management.test.ts: P (exit 0)
round 3 · packages/channels/base/src/ChannelBase.test.ts: P (exit 0)
round 3 · packages/channels/dingtalk/src/DingtalkAdapter.test.ts: P (exit 0)
round 3 · packages/channels/dingtalk/src/interaction-presenter.test.ts: P (exit 0)
round 3 · packages/channels/dingtalk/src/interactive-card-types.test.ts: P (exit 0)
round 3 · packages/channels/dingtalk/src/permission-card-controller.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/channel/daemon-worker.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/channel/runtime.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/channel/start.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/routes/workspace-channel-management.test.ts: P (exit 0)
round 4 · packages/channels/base/src/ChannelBase.test.ts: P (exit 0)
round 4 · packages/channels/dingtalk/src/DingtalkAdapter.test.ts: P (exit 0)
round 4 · packages/channels/dingtalk/src/interaction-presenter.test.ts: P (exit 0)
round 4 · packages/channels/dingtalk/src/interactive-card-types.test.ts: P (exit 0)
round 4 · packages/channels/dingtalk/src/permission-card-controller.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/channel/daemon-worker.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/channel/runtime.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/channel/start.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/routes/workspace-channel-management.test.ts: P (exit 0)
round 5 · packages/channels/base/src/ChannelBase.test.ts: P (exit 0)
round 5 · packages/channels/dingtalk/src/DingtalkAdapter.test.ts: P (exit 0)
round 5 · packages/channels/dingtalk/src/interaction-presenter.test.ts: P (exit 0)
round 5 · packages/channels/dingtalk/src/interactive-card-types.test.ts: P (exit 0)
round 5 · packages/channels/dingtalk/src/permission-card-controller.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/channel/daemon-worker.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/channel/runtime.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/channel/start.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/routes/workspace-channel-management.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
The verification gate rejected the previous commit because packages/cli/vitest.config.ts is test-config machinery this PR never touched. Restore that file exactly to its pre-round state; the rest of the rejected commit (the zh always-allow scope-suffix fix and the per-file test accommodations) is preserved. The environmental gate-leg blocker the reverted change tried to work around — the gate's env -i clean child stripping RUNNER_NAME and QWEN_HOME while passing through a HOME the tests cannot write to — is escalated to the maintainer in the round summary instead.
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — same-run verification repair (PR #10457)Round summary: this is a same-run verification repair round. The previous Feedback points and decisionsDeterministic rejection — round expands into test-config machinery — FIXEDThe gate rejected the previous round with: Fixed: [Critical] Promoted blocker — gate test legs fail environmentally (rv:5083609366 / ic:5491209528) — ESCALATED, needs a maintainer decisionThis finding genuinely requires changes the bot is not allowed to make, so it What is proven (evidence in the workdir):
Why the bot stops here: the durable fixes live in forbidden areas or need a
Open question for the maintainer: which of (a)/(b)/(c) do you want for the Findings resolved by the preserved commit — re-verified this round (8)The preserved rejected commit already implemented these; each was re-verified
All eight are listed in Deferred to the next round (22 findings)R1-11, R1-27, R1-34, R1-26, R1-2, R1-28, R1-12, R1-3, R1-5, R1-1, R1-4, R1-14, Maintainer-deferral comment (ic:5498743239) — no code action possibleAsks for (a) maintainer sign-off on the shared ChannelBase change and (b) one Review gap disclosureThe reviewer noted Conflict notesNone ( VerificationCommands actually run this round, in order:
The two failing legs fail for the stripped-environment classes documented in 中文说明Autofix 评审轮次 — 同轮验证修复(PR #10457)本轮摘要:这是一轮同轮验证修复。上一个提交被确定性门禁拒绝,原因是它修改了 反馈点与决定确定性拒绝 — 轮次扩展到 test-config 机制 — 已修复门禁拒绝上一轮的理由是:「本轮修改了 PR 自身从未触及领域的 CI/验证机制: 已修复: [Critical] 升级阻塞项 — 门禁测试环节因环境原因失败(rv:5083609366 / ic:5491209528)— 已升级,需要维护者决定该发现确实需要 bot 不被允许做出的改动,因此按门禁自身的指令保持未解决, 已证实的事实(证据在工作目录):
bot 在此停下的原因: 持久性修复位于禁区或需要政策决定。维护者可选项:
给维护者的开放问题: 门禁干净子进程的环境,您选择 (a)/(b)/(c) 中的哪一种? 保留提交已解决的发现 — 本轮重新核对(8 项)被保留的被拒提交已经实施了这些发现;本轮逐项对照当前代码重新核对
八项全部列入 顺延至下一轮(22 项发现)R1-11、R1-27、R1-34、R1-26、R1-2、R1-28、R1-12、R1-3、R1-5、R1-1、R1-4、 维护者转交评论(ic:5498743239)— 无可执行的代码动作要求(a)维护者对共享 ChannelBase 变更签核,(b)执行一次真实钉钉验证。 审查缺口披露审查者注明 冲突说明无( 验证本轮实际执行的命令,按顺序:
两个失败环节的失败原因是升级项中记录的剥离环境类别(RUNNER_NAME/QWEN_HOME Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
Local end-to-end verification — Linux, full DingTalk stack, no networkI built a real verification environment for this PR and ran the whole feature against it. Verdict: the feature does what the description says, and I found nothing blocking. 40 live end-to-end checks pass with 0 failures, on the current head Since the description lists Linux as What was actually runNo mocks in the channel path. The unmodified vendor
Arms: base = merge-base The card renderings above are drawn from the exact Coverage against the PR's own reviewer test plan
I also checked the two things most likely to break by accident: a group-chat run (card delivered to Did the tests earn it?Mutation matrix on the PR's own suites — 8/8 mutants killed: dropping the owner binding, letting terminal cards be re-claimed, dropping the offered-decisions whitelist, accepting extra form fields, reverting the duplicate-response race fix, ignoring the Stronger: I rebuilt a third full bundle with the owner-binding block physically deleted and re-ran the live check. An outsider's callback then settled the permission and the card went Suites, lint, typecheck (on
|
| 组件 | 真实之处 |
|---|---|
| 钉钉开放接口 | /etc/hosts 把 api.dingtalk.com、oapi.dingtalk.com 指向 127.0.0.1;自签 CA(经 NODE_EXTRA_CA_CERTS 信任)在 :443 提供 HTTPS,实现 gettoken、gateway/connections/open、card/instances/createAndDeliver、card/instances(PUT)、card/streaming、robot/* |
| 流式通道 | 真实 WebSocket 网关先推 SYSTEM CONNECTED/REGISTERED,再推 /v1.0/im/bot/messages/get 与 /v1.0/card/instances/callback 的 CALLBACK 帧 |
| 模型 | 脚本化的 OpenAI 兼容服务,先发一个需要审批的 run_shell_command(touch PERMIT_10457.txt),再给最终回答 |
| 判据 | daemon 实际发出的 HTTP body 字节,以及工具是否真的在磁盘上落了文件 |
两臂:base = merge-base 56f92c8,PR = d4fc363,另有一个故意改坏的第三份构建作为反向对照(见下)。
上方卡片截图是按 daemon 实际发出的 cardParamMap 渲染的,属于 harness 对钉钉表单卡片的近似还原,不是钉钉客户端截图。
对照 PR 自带的 Reviewer 测试计划
| 测试步骤 | 结果 |
|---|---|
| 1. 只出现原生卡片而非权限命令文本 | 卡片以 question 模板下发;卡片存续期间完全没有发送 /approve 文本 |
| 2. 允许一次 / 拒绝各只结算一次 | 均只结算一次、各自只有一次终态投影;允许时 touch 文件出现,拒绝时始终不出现 |
| 3. 仅在提供时展示持久授权,且沿用原始范围 | exec 确认下为 3 个选项(含 Always Allow in project: touch *),选中后同一会话第二次相同调用不再产生新卡片;用 permissions.ask 强制 ask 时只有 2 个选项,此时手工构造的 allow_always 提交被拒绝——不能提权到从未提供过的授权 |
| 4. 取消与超时都在延迟更新可能与文本批准竞争之前拒绝 | 取消 → cancelled;超时(timeoutMs: 6000,实测约 6031 ms 触发)→ expired;工具从未执行;迟到的 /approve 无法复活已失效请求;从状态卡停止运行同样会取消待处理权限卡 |
| 5. 他人 / 畸形 / 重复 / 过期均不能结算 | 非发起人回调被拒(且只提醒一次,不重复);6 种畸形载荷被拒(空表单、未知值、两个值、多余字段、字段名错误、无业务载荷);未知 outTrackId 被忽略;终态卡片无法重复提交;非发起人的 /approve 文本命令同样被拒 |
| 6. 关闭卡片或投递失败时回退文本 | 两条路径都回退,且回退文案与 main 逐字节一致(247 字节) |
| 7. 语言取启动时默认值,不做 session 查询 | 覆盖 6 种语言配置,且两条接线路径(serve --channel daemon worker 与 channel start)都验证过;整轮运行中 daemon 只收到 /capabilities、/session、/prompt、/permission,没有任何语言查询 |
另外我特意验了两处最容易被顺手改坏的地方:群聊运行(卡片投递到 IM_GROUP、群内发出仅发起人可操作的提示、其他成员被拒),以及 ask_user_question——它仍然渲染为 question 卡片、仍由自己的 controller 结算,说明把权限回调放在路由最前面并没有把它吞掉。
测试本身立得住吗
对 PR 自带用例做变异测试,8/8 变异体全部被杀:去掉发起人绑定、允许终态卡片被重新认领、去掉「仅接受已提供选项」白名单、允许表单夹带多余字段、回退重复响应竞态修复、忽略 locale 选项、去掉下划线归一化、回退新增的授权范围后缀保留。
更强的一层:我另外完整构建了第三份 bundle,把发起人绑定那段代码物理删除后重跑实测——外部用户的回调随即结算了权限、卡片进入 approved。这证明我的实测检查是在鉴别那道守卫,而不只是在观察绿色。
套件、lint、typecheck(d4fc363,Linux)
ChannelBase.test.ts 645/645 · packages/channels/dingtalk 512/512 · 受影响 CLI 套件 174/174 · 所有改动源文件 eslint --max-warnings 0 干净 · channels/base 与 channels/dingtalk tsc --noEmit 干净。(我这套 link 出来的 worktree 里跑全量 packages/cli typecheck 会因缺少一个可选依赖而报错,全部集中在 src/ui/opentui/,没有一条落在本 PR 改动的文件上。)
非阻塞问题
1. 出厂默认语言传不到卡片。 resolveChannelLocale() 读的是 general.language 原始值,而 CLI 自身的 i18n 会通过 QWEN_CODE_LANG || LANG 解析 auto,并且 QWEN_CODE_LANG 优先级高于配置项。实测:language: "auto"(schema 默认值)配合 LANG=zh_CN.UTF-8 时权限卡片是英文,而 CLI 自己的解析器对同一个值给出的是中文(resolveSupportedLanguage('zh_CN.UTF-8') === 'zh');QWEN_CODE_LANG=zh 而未写配置时是同样的分叉。既然动机是「避免中文 Channel 体验里出现英文权限文案」,那么从未打开过设置对话框的用户就享受不到这个改动。复用 resolveLanguageSetting() + detectSystemLanguage() 即可闭合。
2. zh-TW 拿到的是简体文案。 language.startsWith('zh-') 把繁体中文映射到 zh 文案,而 CLI 本身是把 zh-TW 作为独立语言发布的。
3. 会话现在是中英混排。 language: "zh" 下权限卡片是中文,但状态卡(Running · 0s)和 ask_user_question 卡片(Submitted.)仍是英文。这在本 PR 范围之外,但目标只完成了一半。
4. 文本批准会把卡片置为「已失效」。 卡片还在时发起人输入 /approve,请求确实被批准、工具确实执行了,但卡片被投影为 expired,文案是 "This permission request is no longer available."。对卡片而言准确,对结果而言容易误解。改成「已在别处处理」之类的措辞,或复用已授权文案,读起来会更准。
5. 升级影响建议在描述里补一句。 interactiveCards.permissionCard.enabled 默认 true,因此所有已经配置了 interactiveCards 的现网部署,升级后无需改任何配置就会从文本命令切换到卡片。这大概率是有意为之,但对现有用户来说是行为变更,而不是可选开启。
一个待确认的问题
权限卡片复用的是已发布的 question 模板(c2a6355b-…),并引入了两个 question 卡片从不发送的 card_status 取值——approved 与 denied(question 卡片只会发 pending、submitted、expired、cancelled)。设计文档写的是「pending、approved、expired 等协议卡片状态保持不变」,但对这个模板来说 approved/denied 是新增值。我的 harness 无法回答已发布模板是否会把它们渲染成不可再操作的终态——这正好是你已经列为「未验证」的那条「使用已发布模板的真实投递」。建议合入前补一张真实工作区的截图,或者把这两个新状态映射到 submitted/cancelled。
其他
PR 目前对 main 有冲突,只有一个文件:packages/cli/src/ui/AppContainer.test.tsx。
描述里的「Tested on」表格可以补上:🐧 Linux ✅(本轮)。
🤖 Verified with Claude Code — Claude Opus 5 (1M context)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
22 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 canEnvelopeAnswerPendingPermission owner-binding clause unwitnessed — already reported (comment 3908905688)
- R1-2 tryPresentPermission isUserQuestion exclusion unwitnessed — already reported (comment 3908905657)
- R1-3 presenter-throws fallback branch untested — already reported (comment 3908905677)
- R1-4 adapter-to-controller locale wiring no end-to-end witness — already reported (comment 3908905706)
- R1-5 native-seam deny mapping unwitnessed — already reported (comment 3908905682)
- R1-6 default-enabled permissionCard not documented in user docs — already reported (comment 3908905727)
- R1-8 controller rejected/throwing responder branch unwitnessed — already reported (comment 3908905766)
- R1-11 committed plan-doc snippets contradict the shipped implementation — already reported (comment 3908905623)
- R1-12 permissionPresented reset on unsupported fallback unwitnessed — already reported (comment 3908905669)
- R1-14 permission-before-question routing only witnessed permission-wins — already reported (comment 3908905716)
- R1-15 finishTerminalProjection branch never exercised (no harness passes reserveRunProjection) — already reported (comment 3908905741)
- R1-19 DM-branch feedback copy unwitnessed — already reported (comment 3908905722)
- R1-23 updateInstance rejection never tested — already reported (comment 3908905818)
- R1-26 presentation-contract handled success branch unwitnessed — already reported (comment 3908905651)
- R1-27 permission-side onSettled closure unwitnessed — already reported (comment 3908905631)
- R1-28 staleness guards in tryPresentPermission unwitnessed — already reported (comment 3908905663)
- R1-29 settlement-listener non-resolved branch never exercised — already reported (comment 3908905825)
- R1-31 expire() state guard unwitnessed — already reported (comment 3908905811)
- R1-34 zh PERMISSION_COPY tool/parameters/request keys unwitnessed — already reported (comment 3908905643)
- R1-35 presentPermission DM delivery mapping unwitnessed — already reported (comment 3908905734)
- …and 2 more (see the run report)
Not reviewed: build-and-test — the packages/cli suite timed out locally under the harness budget on a saturated host (infrastructure result; the failures observed during the timed-out runs are all in files this PR does not touch, and CI checks at the reviewed commit all passed).
Not explored to full depth (tool budget reached): chunk 7: executing src/interactive-card-types.test.ts and src/permission-card-controller.test.ts via vitest — no node_modules in the review worktree or parent checko…; chunk 8: executing permission-card-controller.test.ts (worktree has no node_modules and no built packages/channels/base/dist ; npm ci + build exceeds the tool bud….
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/channels/base/src/ChannelBase.ts:3187 — [probe] zh 'Allow All Edits' always-allow label drops the scope qualifier (renders '始终允许' instead of '允许所有编辑')packages/channels/base/src/ChannelBase.test.ts:1841 — [probe] permission-side output-segment handling in tryPresentPermission has no witness (user-input twin is pinned)packages/channels/base/src/ChannelBase.ts:3395 — [probe] card-presented command-answer success path of the new '|| pending.permissionPresented' clause unwitnessed (settlement-reason semantics unpinned)docs/design/2026-08-29-dingtalk-permission-cards.md:18 — [review] design doc says 'single-choice permission field' but the implementation ships a CHECKBOX_GROUP (contradicts its own line 52)packages/channels/dingtalk/src/permission-card-controller.test.ts:287 — [probe] known-but-unadvertised decision kind has no witness against the claim gate (defense-in-depth)packages/channels/base/src/ChannelBase.test.ts:2128 — [probe] second-caller race unwitnessed when the in-flight bridge response rejects (loser path settles 'cancelled' + failure feedback)
[Critical] Promoted blocker from the autofix bot's issue-level comment (comment 5491209528), re-checked this round and still standing: the review-address verification gate's packages/cli test leg fails for environmental reasons — the gate's env -i clean child (.github/workflows/qwen-autofix.yml:5431) allowlists only PATH, HOME, RUNNER_TEMP, WORKDIR, BRANCH, GITHUB_OUTPUT, CI, KISS_AUDIT, and FOOTPRINT_ENFORCE, so RUNNER_NAME and QWEN_HOME are stripped, disabling the RUNNER_NAME-keyed slow-host budgets in packages/cli/vitest.config.ts (testTimeout/hookTimeout/maxWorkers, still keyed to RUNNER_NAME starting with ecs-qwen- at the reviewed commit). This PR's test-side accommodations (isSlowTestHost() loadavg fallback, scratch HOME dirs) mitigate part of the surface, but the config-level accommodation reverted by d4fc363 keeps the leg exposed on saturated non-ECS hosts, as the autofix bot's round-4 repair measured. Escalated to the maintainer in autofix round 4 with options (a)/(b)/(c); unresolved at the reviewed commit.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 22 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — the packages/cli suite timed out locally under the harness budget on a saturated host (infrastructure result; the failures observed during the timed-out runs are all in files this PR does not touch, and CI checks at the reviewed commit all passed)。
未探索到全部深度(达到工具调用预算):chunk 7:executing src/interactive-card-types.test.ts and src/permission-card-controller.test.ts via vitest — no node_modules in the review worktree or parent checko…;chunk 8:executing permission-card-controller.test.ts (worktree has no node_modules and no built packages/channels/base/dist ; npm ci + build exceeds the tool bud…。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。
[Critical] Promoted blocker from the autofix bot's issue-level comment (comment 5491209528), re-checked this round and still standing: the review-address verification gate's packages/cli test leg fails for environmental reasons — the gate's env -i clean child (.github/workflows/qwen-autofix.yml:5431) allowlists only PATH, HOME, RUNNER_TEMP, WORKDIR, BRANCH, GITHUB_OUTPUT, CI, KISS_AUDIT, and FOOTPRINT_ENFORCE, so RUNNER_NAME and QWEN_HOME are stripped, disabling the RUNNER_NAME-keyed slow-host budgets in packages/cli/vitest.config.ts (testTimeout/hookTimeout/maxWorkers, still keyed to RUNNER_NAME starting with ecs-qwen- at the reviewed commit). This PR's test-side accommodations (isSlowTestHost() loadavg fallback, scratch HOME dirs) mitigate part of the surface, but the config-level accommodation reverted by d4fc363 keeps the leg exposed on saturated non-ECS hosts, as the autofix bot's round-4 repair measured. Escalated to the maintainer in autofix round 4 with options (a)/(b)/(c); unresolved at the reviewed commit.
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 5/100) — it will retry on the next scan.
See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(idle-timeout (no output for 1200000ms — the sandbox likely hung at startup))(第 5/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/33594258815 🧠 Handled by Qwen Code · model/模型 |
|
🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 5/100) — it will retry on the next scan.
See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 5/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/33601284863 🧠 Handled by Qwen Code · model/模型 |
…0457) A timed-out permission card fired respond('deny') and then suspended on the terminal card projection, leaving the denial promise without a handler for the whole window. When the bridge rejected inside it, the rejection escaped as a process-level unhandledRejection before the trailing catch ever attached, and Node logged a second rejectionHandled warning once the late await picked it up. Attach the handler where the promise is created so the expiry denial is reported as soon as it fails. The claimed-state assignment stays ahead of the call, and respond still precedes the projection.
…ion-cards-10388 # Conflicts: # packages/cli/src/ui/AppContainer.test.tsx
|
@qwen-code /resolve |
…t work (QwenLM#10457) AppContainer's mount effect runs the real config.initialize() in an un-awaited IIFE, so extension-store lock work against this suite's scratch HOME can still be queued when afterAll runs. Deleting the tree there fails that work with ENOENT, which withLock rethrows as ExtensionStoreBusyError into a promise nobody awaits: all 171 tests pass and the packages/cli leg still exits non-zero on one unhandled rejection. Leave the tree for the OS to reclaim. Deferring the deletion to a process 'exit' handler was measured first and is not an option here — vitest workers in this repo never run it.
|
🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #10457 (address-review, same-run verification repair)One additive follow-up commit on top of the two preserved commits from the
Critical-only mode is active and the round carried a budget warning, so the The supplied deterministic rejection
Root cause — this PR's own test accommodation, measured not inferredThree facts compose into the failure:
Evidence, in the order it was taken:
Correction to the escalated CriticalThe promoted Critical (
So the operative cause was this PR's own deletion race, and that is what this Fix
The alternative I built and then discarded is worth recording, because it looks Cost, stated plainly rather than hidden: one small scratch tree (tens of KB) per No committed witness test pins this, and the reason is structural rather than Dispositions
Changes
VerificationEvery command below was actually run; the two test legs were run with the gate's
Not run, and why: integration tests after One local-only artifact worth recording so nobody chases it: my first leg run, 中文说明Autofix 轮次总结 —— PR #10457(address-review,同轮验证修复)在被拒绝那一轮的两个提交之上,新增一个增量提交。未改写历史,未 amend:
当前处于仅处理 Critical 的模式,且本轮带有预算警告,因此批次压缩为最小的阻塞子集:即导致确定性门禁拒绝上一个提交的那一个缺陷。 门禁给出的确定性拒绝
根因 —— 本 PR 自己的测试适配,实测得出而非推断三个事实叠加成了这次失败:
证据,按取得顺序:
对被提升为 Critical 的那条的更正被提升的 Critical(
因此真正起作用的原因是本 PR 自己的删除竞态,而这正是本轮用代码修掉的东西。该升级条目提到的残留部分 —— 那些通过运行时 修复
我先做了、随后又放弃的另一个方案值得记录,因为它看起来更像是「更整洁」的修法:把删除延后到 代价直说,不藏:这个 suite 每跑一次,就会在系统临时目录里留下一棵小的临时目录树(几十 KB),交给 tmp 清理机制处理。这不是一类新的泄漏 —— 修复前的代码只要竞态命中就会留下同样的一棵树(上面实测到三次)。这次修复把「偶发残留」换成了「必然残留」,换来的是一条不再让整轮失败的测试段。 本轮没有提交任何用于固定该行为的测试,原因是结构性的,不是疏忽:该缺陷只在 suite 拆除阶段显现,vitest 把它记为运行级的 处置
改动
验证下面每条命令都是实际执行过的;两条测试段都使用门禁的
未运行的命令及原因: 没有在 有一条仅存在于本地的现象值得记录,以免有人去追:我第一次跑测试段时用的是 agent shell 的环境而不是门禁的环境,结果在 Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。 🧵 Resolved all 1 selected review thread(s). · 已关闭全部选中的 1 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
Qwen Code did not run conflict resolution for this request. PR #10457 does not currently have merge conflicts with main. |
|
Qwen Code review was cancelled before a review could be posted. Nothing failed and nothing is retried automatically: the run was cancelled — by an operator, an upstream event, or the job exceeding its execution time limit. If you still want a review of this PR, request one with |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-2 tryPresentPermission eligibility gate no negative test — already reported (comment 3908905657)
- R1-8 controller accepted===false/'expired' responder branch unwitnessed — already reported (comment 3908905766)
- R1-1 canEnvelopeAnswerPendingPermission permissionPresented owner-binding no non-owner witness — already reported (comment 3908905688)
- R1-3 presenter-throws fallback branch untested — already reported (comment 3908905677)
- R1-4 adapter-to-controller locale wiring no end-to-end zh witness — already reported (comment 3908905706)
- R1-11 language-plan copy-constant snippets contradict shipped constants (items 4+5) — already reported (comment 3908905623)
- R1-29 settlement-listener non-resolved branch unwitnessed — already reported (comment 3908905825)
- R1-23 projectTerminal updateInstance-rejection guard unwitnessed — already reported (comment 3908905818)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
docs/design/2026-08-29-dingtalk-permission-cards.md:76 — [probe] terminal-state table says every outside resolution projects 'expired', but the code projects 'cancelled' when the outside resolution rejected (owner /deny text command)packages/cli/src/commands/channel/runtime.ts:31 — [probe] resolveChannelLocale canonicalizes fewer Chinese aliases than the CLI's resolveSupportedLanguage ('Traditional Chinese', '繁體中文', 'zh.UTF-8' resolve Chinese for the UI but English cha…
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)









What this PR does
This PR presents attended DingTalk tool-permission requests as native interactive cards. The card offers allow once, deny, and the persistent allow choice only when the original permission request advertises it. Responses are bound to the originating user and live run, settle the original request at most once, and move stale, cancelled, denied, approved, or expired cards into a terminal state.
Existing text commands remain the fallback when permission cards are disabled or card delivery fails. At Channel startup, the already-loaded default
general.languagesetting selects Chinese permission copy for Chinese values and English for all other values. The daemon follows the same startup-snapshot behavior and does not call the per-session language endpoint. Other channels do not gain native permission cards, and permission-policy semantics are unchanged.Why it's needed
DingTalk already uses native cards for task status and structured questions, but ordinary tool permissions still require users to type
/approve,/approve-always, or/deny. A native card makes this security-sensitive interaction clearer and easier to operate while preserving the exact choices supplied by the permission system. Following the existing default language also avoids showing English permission chrome inside a Chinese Channel experience.Reviewer Test Plan
How to verify
/approve, optional/approve-always, and/denytext instructions are delivered.zh_CN, then withenorauto. Confirm permission card chrome, terminal states, owner-only feedback, and fallback copy are Chinese forzh_CNand English otherwise; no session-language lookup is required.Evidence (Before & After)
Before: ordinary DingTalk tool permissions are rendered as text commands that the user must type manually, and the permission flow does not consistently follow the configured default language.
After: ordinary attended permissions are rendered as an owner-bound native card with only the choices supplied by the original permission request; terminal cards cannot be reused, and the card plus immediate fallback follow the default Channel language. No live DingTalk screenshot is included because valid app credentials and published-template delivery evidence were not available in the local environment.
Automated evidence: 639 ChannelBase tests, 335 affected DingTalk tests, and 171 affected CLI tests pass. Repository build, typecheck, lint, targeted formatting, and diff checks pass. Review findings covering callback races, Chinese fallback title selection, and underscore locale normalization were fixed with regression coverage; two final clean review passes found no remaining blocking issue.
Tested on
Environment (optional)
macOS 14.4.1, Node.js v25.2.1, local package tests and monorepo validation. Real DingTalk delivery was not run.
Risk & Scope
Linked Issues
Closes #10388
中文说明
本 PR 的改动
本 PR 将钉钉中有人值守的工具权限请求展示为原生交互卡片。卡片提供“仅允许一次”“拒绝”,并且仅当原始权限请求明确提供持久授权时才展示该选项。响应会绑定到发起用户和当前运行,同一原始请求最多结算一次,过期、取消、拒绝、允许或超时的卡片都会进入不可再次操作的终态。
当权限卡片被禁用或卡片投递失败时,现有文本命令仍作为回退。Channel 启动时只读取已加载的默认
general.language配置:中文配置使用中文权限文案,其余配置使用英文。daemon 同样使用启动时快照,不调用 session 级语言接口。其他渠道不会获得原生权限卡片,权限策略语义保持不变。为什么需要此改动
钉钉渠道已经使用原生卡片展示任务状态和结构化问题,但普通工具权限仍要求用户手动输入
/approve、/approve-always或/deny。原生卡片让这一安全敏感交互更清晰、更易操作,同时严格保留权限系统提供的原始选项。沿用已有默认语言也可以避免中文 Channel 体验中出现英文权限卡片文案。Reviewer 测试计划
如何验证
/approve、可选/approve-always和/deny文本说明。zh_CN后启动或重载 Channel,再分别使用en或auto。确认权限卡片文案、终态、非发起人反馈和回退文案在zh_CN下为中文,其他配置下为英文,并且不依赖 session 语言查询。证据(改动前后)
改动前:普通钉钉工具权限以文本命令展示,用户必须手动输入命令,并且权限流程没有稳定沿用已配置的默认语言。
改动后:普通的有人值守权限以仅原发起人可操作的原生卡片展示,并且只包含原始权限请求提供的选项;终态卡片无法重复使用,卡片和直接回退文案遵循默认 Channel 语言。由于本地环境没有可用的应用凭证和已发布模板投递证据,因此未附真实钉钉截图。
自动化证据:639 个 ChannelBase 测试、335 个受影响钉钉测试和 171 个受影响 CLI 测试通过。仓库构建、类型检查、lint、目标格式检查和差异检查均通过。评审发现的回调竞态、中文回退标题选择和下划线语言配置归一化问题均已修复并补充回归覆盖;最终两轮复核未发现剩余阻断问题。
测试平台
环境(可选)
macOS 14.4.1、Node.js v25.2.1,本地包测试与 monorepo 校验。未运行真实钉钉投递。
风险与范围
关联 Issue
Closes #10388