Skip to content

ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848

Description

@yinlianghui

来源

发现自 #3842 实施(渲染器侧 disabled 的「已声明」判定补 !== '')的同族扫面。不在 #3842 的修法面(PM 已裁其范围为 DeclaredActionsBar + action:button 两处渲染落点),故独立成单,未认领。

机理

packages/core/src/actions/ActionRunner.ts:666 的执行门:

if (action.disabled != null && action.disabled !== false) {
  let isDisabled = false;
  try { isDisabled = this.evaluator.evaluateCondition(action.disabled as never); } catch { isDisabled = false; }
  if (isDisabled) return { success: false, error: 'Action is disabled' };
}

两点都与渲染器侧不同:

  1. 入门判定是 != null && !== false,没有 !== '' —— 空谓词进门;
  2. 把 原始值交给 evaluateCondition,而不是先过 toPredicateInput。evaluateCondition 对空/纯空白/空 source 的语义是「没有条件 → true」,而这里 true 意味着禁用。

于是任何「空谓词」拼法都被判成「已禁用」并拦掉执行。

实测(worktree @ origin/main = aca561a77fd0dc793ea76a7b46cd037e1731eb93,一次性探针,未提交)

new ActionRunner({}) + registerHandler('probe'),只改 disabled 一个键:

disabled ''                       | handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled '   '                    | handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled {dialect:'cel',source:''}| handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled absent                   | handler ran: true  | result: {"success":true}
disabled false                    | handler ran: true  | result: {"success":true}

handler 一次没跑 —— 不是「点了没反应」,是执行被门拦下并回了一个 Action is disabled 错误。

与渲染器侧的分歧(#3314 同一类)

同一个 disabled 值,两条路给出相反答案(渲染器侧数据取自同一批探针,PR 落地后):

disabled 渲染器(修完 #3842 后) ActionRunner.execute
'' 可点 拦
' '(纯空白) 可点('${ }' 求值为 false) 拦
{dialect:'cel', source:''} 置灰(#3842 残留,另单) 拦
false / 未声明 可点 放行

所以 #3842 修完之后的用户可见行为是:disabled: '' 的服务端声明动作按钮能点了,点下去收到 Action is disabled 报错。比原来「永久置灰、无任何解释」是改善(至少有了消息),但这半边不修,该动作仍然执行不了。渲染器与执行入口对同一谓词不同意,正是 #3314 已经付过一次代价的形状。

影响

修法建议(未裁)

契约优先:「是否声明了 disabled 门」这个问题在仓里已有唯一定义(hasDeclaredVisibilityGate,packages/components/src/renderers/action/visibility-gate.ts,!= null && !== ''),而「谓词如何归一」也已有唯一定义(toPredicateInput,packages/core/src/evaluator/predicateInput.ts)。这处执行门两个都没用。

方向:入门判定改问「已声明」,并把值过 toPredicateInput 后再求值,使执行侧与渲染侧逐一同意。注意 hasDeclaredVisibilityGate 目前住在 components,而这里是 core(依赖方向相反)—— 定义大概要下沉到 core(与 toPredicateInput 同层)才谈得上复用,那是跨包动作,请先裁。⚠️ 现有 catch { isDisabled = false }(求值失败不误拦)的姿态应保留。

Related: #3842(渲染器侧同族,PR: claude/issue-3842-disabled-declared-gate)、#3492(不变量出处)、#3314(渲染路径与引擎路径判定分歧的先例)、#1885(disabled 首次接线)。未认领。

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue (in-repo card — sibling repos take no domain:*).

    Classification basis: concrete defect with a scripted probe (handler provably never runs; execution returns Action is disabled for every empty-predicate shape) at a named location. Premise re-verified on origin/main @ 993336f — which is exactly #3842's merge (PR #3851): the gate at packages/core/src/actions/ActionRunner.ts:666 still reads action.disabled != null && action.disabled !== false with no empty-string half and passes the raw value to evaluateCondition, so the renderer/executor disagreement the card describes is now live user-visible behavior (button clickable per #3842, execution still blocked).

    Direction note for dispatch: the behavioral invariant is already ruled (#3492 declared-gate + toPredicateInput as the single normalizer, #3842 precedent) — align the execution gate with the renderer verdict. The open sub-question (whether the declared-gate helper moves from components to core, dependency direction) is an implementation-placement choice for the implementer/reviewer, not a product ruling; routing the value through toPredicateInput also covers the empty-envelope shape, which keeps this card independent of #3850's definition-scope decision. Preserve the existing catch { isDisabled = false } fail-open posture.

    Dedup: repo-scoped search finds only the family itself (#3842 merged two renderer sites; #3849 five remaining renderer sites; #3850 shared-definition scope) — file faces are disjoint (this card is packages/core only). No open PR covers this gate.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 8, 2026
  3. yinlianghui commented on Aug 8, 2026

    @yinlianghui
    CollaboratorAuthor

    🔒 认领(objectui shard PM,session session_01GTRjn8xBqp75dk7kFupVRt)


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions