fix(core): ActionRunner.execute 的 disabled 门改问「有没有条件」,空谓词不再拦掉执行 (#3848) - #3873
Conversation
执行门原来问 `action.disabled != null && action.disabled !== false`,然后把原始值
直接交给 `evaluateCondition` —— 而那个函数对「这里没有条件」只有一个默认答案:
返回 `true`(即 visible/enabled)。在 `disabled` 这个键上,`true` 的意思是「已禁用」。
于是任何空谓词都被判成禁用:handler 一次不跑,调用方收到一个元数据从未声明过的
`{ success: false, error: 'Action is disabled' }`。
实测(计数 handler,基线 origin/main @ f0a625a):
disabled '' | handler ran: false | Action is disabled
disabled ' ' | handler ran: false | Action is disabled
disabled {dialect:'cel',source:''} | handler ran: false | Action is disabled
disabled absent / false | handler ran: true | success
#3842 / #3849 修完渲染器两侧之后,这已经是活的用户可见行为:按钮能点了,点下去
收到 `Action is disabled` —— 渲染器与执行入口对同一个谓词值给出相反答案,正是
#3314 已经付过一次代价的形状。
新门先问「有没有条件可判」再求值:「没有条件」的判定取自 core 唯一的谓词归一器
(`toPredicateInput`,把 `''`/`null`/空 `source` envelope/非谓词值映射为
`undefined`),外加纯空白字符串 —— 归一器对它是包裹而非折叠,而 `evaluateCondition`
自己就把空白 `trimmed` 叫做「没有条件」(`evalRowPredicate` 用的也是这条 blank-source
规则)。
行为变更面单向且刻意窄:只有「没有东西可求值」的形状变化,且一律从「拦」变「放行」
—— `''`、纯空白、空 `source` envelope,以及非谓词垃圾值(`0`、`{}`,原先被强制成
「已禁用」)。`disabled: true`、取真表达式、取真 CEL envelope 仍拦;`disabled: false`
与未声明仍放行;任何表达式/envelope 的 verdict 一律不变。`catch { isDisabled = false }`
失败放行姿态原样保留。
交给求值器的值刻意仍是**原始值**,不是归一结果:对已经是 `${…}` 模板的字符串,
`toPredicateInput` 会二次包裹成 `'${${x}}'`,解析失败后原样退回,`Boolean(非空串)`
恒为 `true` —— 那样求值的模板谓词永远「已禁用」。该归一器缺陷另立 #3871(动作面
渲染器与 ActionEngine 都中招,而 SchemaRenderer 与 page:header 读原始值、钉的是正确
verdict),钉子旁留了 tripwire,#3871 修好那天它会红。因此仍有两行两侧不一致,各自
写明归属:空 envelope(渲染器半边归 #3850 的范围裁决)与 `${…}` 拼法(#3871)。
反向验证(方向先判后跑):还原旧门、求值不动 → 恰好五行「没有条件」钉子变红
(`''`、`' '`、空 envelope、`0`、`{}`)并各自点名形状,外加「空谓词根本不该到达
求值器」那条;`absent`/`false`/`true`/两个表达式行/两个非空 envelope 行全绿
(6 failed | 14 passed),与预判一致。
Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收(PM,session 实物核验:头 偏离裁定:派发词的优先形状(求值归一结果)被 dev 逐形状实测证伪 ——
转 ready 并挂 auto-merge。#3871(归一器二次包裹,A 修生产端→B spec 校验的裁决建议)与 #3872(condition 门真值判定过度放行)归分诊席定级;#3871 与 #3850/#3862 同族(空谓词/定义层三联),建议分诊席并案裁。#3850 落地后本门收敛共享定义(dev 荐 A)记入该单待办。 Generated by Claude Code |
Fixes #3848
基线
origin/main=f0a625aa7b00c93a48329e1c456b1aeacfceba43(即 #3849 / PR #3861 落地那一提交)。文件面仅packages/core。机理
ActionRunner.execute的执行门问的是action.disabled != null && action.disabled !== false,然后把原始值直接交给evaluateCondition—— 而那个函数对「这里没有条件」只有一个默认答案:返回true,含义是 visible/enabled。在disabled这个键上,true的意思恰好相反,是「已禁用」。于是任何空谓词都被判成禁用:handler 一次不跑,调用方收到一个元数据从未声明过的{ success: false, error: 'Action is disabled' }。#3842 / #3849 修完渲染器两侧之后,这已经是活的用户可见行为:按钮能点了,点下去收到
Action is disabled。同一个谓词值,渲染器与执行入口给出相反答案 —— #3314 已经付过一次代价的形状。逐形状新旧对照(计数 handler,同一张探针表跑基线与改后)
「拦」= handler 未被调用且返回
Action is disabled;「跑」= handler 被调用、success: true。disabled''' '(纯空白){ dialect: 'cel', source: '' }falsetrueuser.role == "admin"user.role == "guest"{ dialect: 'cel', source: 'true' }{ dialect: 'cel', source: 'false' }'${user.role === "admin"}'(模板取真)'${user.role === "guest"}'(模板取假)0(非谓词){}(非谓词)变化只有加粗那五行,方向一律是「拦 → 放行」。
修法
新门先问「有没有条件可判」,再求值。「没有条件」的判定取自 core 唯一的谓词归一器
toPredicateInput(把''/null/空sourceenvelope/非谓词值映射为undefined),外加纯空白字符串 —— 归一器对它是包裹而非折叠,而evaluateCondition自己就把空白trimmed叫做「没有条件」,evalRowPredicate(evaluator/listConditional.ts)用的也是这条 blank-source 规则。catch { isDisabled = false }失败放行姿态原样保留。为什么求值仍读原始值,而不是归一结果
派发时的优先形状是「先
toPredicateInput归一,再据归一结果求值」。逐形状实测发现这个复合对已经是${…}模板的字符串不成立:toPredicateInput假定入参是裸表达式、无条件包裹,'${x}'变成'${${x}}',单模板快路/^\$\{([^}]+)\}$/不再匹配,内层解析失败后 catch 里原样退回,Boolean(非空串)恒为true。也就是说那样求值的模板谓词永远「已禁用」,与表达式取值无关。实测:disabled: '${user.role === "guest"}'(admin context,取假)基线放行 → 归一求值后被拦。这是本 PR 唯一偏离派发形状的地方:归一只用于判门,verdict 仍读原始值,于是每个非空形状的 verdict 逐一不变,本改动只可能少拦、不可能多拦。归一器的二次包裹缺陷另立 #3871(动作面渲染器与
ActionEngine都以该复合书写,因此都中招;而SchemaRenderer与page:header读原始值、各自有钉子钉住正确 verdict)。钉子里留了指向 #3871 的 tripwire,该单修好那天它会红,提示可以让本门改读归一结果。仍然不一致的两行都写明归属,不在本单顺手修:空
sourceenvelope 的渲染器半边归 #3850(「空谓词」范围裁决,在队),${…}拼法归 #3871。⛔ 未碰components/visibility-gate.ts、未搬 helper 进 core、未碰packages/react(#3862)。钉子
packages/core/src/actions/__tests__/ActionRunner.disabledGate.test.ts(表驱动,形状抄 PR #3851 / #3861 的钉子风格):14 形状逐一断言 handler 跑没跑;一条断言空谓词根本不该到达求值器(门决定,不是 verdict 决定);一条断言「已声明 parity 的每一行,执行侧 verdict 等于渲染器侧 verdict」(渲染器列是从 #3848 正文表转录的证据 —— core 是渲染器包的被依赖方,不能反向 import,活的渲染器钉子在 #3842/#3861 的测试里),并把两行故意不声明 parity 的形状连同归属单号一起钉住;另有机理钉:evaluateCondition(undefined) === true这条反向默认值,以及 #3871 的 tripwire。反向验证(方向先判后跑,预判写在钉子头注释里)
还原旧门(
!= null && !== false)、求值不动 → 预判「恰好五行『没有条件』变红并各自点名形状,非空形状全绿」。实跑:与预判一致(第六条红的是那条「不该到达求值器」的钉子,同属这条门)。
验证
全仓扫过执行门的消费半径:
Action is disabled的断言只在packages/core;components/plugin-detail里的disabled: ''用例全是渲染器侧钉子,不经ActionRunner.execute,未受影响。顺手发现(已另立,未在本 PR 修)
${…}模板的字符串二次包裹,动作面的谓词一律判成 true(visible 永远显示 / disabled 永远置灰) #3871 ——toPredicateInput对已经是${…}模板的字符串二次包裹,动作面渲染器与ActionEngine因此把模板谓词一律判成 true(visible永远显示 /disabled永远置灰),而SchemaRenderer与page:header对同一拼法是对的(各有绿钉子)。condition门用真值判定(if (action.condition)),于是condition: false(最明确的「永不执行」)照样执行,实测 handler 跑了;而同义的{ dialect: 'cel', source: 'false' }被拦。components/action/**: 同一真值可见性门在动作面上未收口 ——action:group/action:menu成员动作声明visible: false照样渲染(#3758 的同族部分) #3812 的形状,方向是过度放行。Generated by Claude Code