Skip to content

record:* 家族与非 objectName 绑定的行为证据——binding-reach 覆盖 14→24/57,#4413 的原始形态不再是假设 #3149

Description

@os-zhuang

#3146 落地的 public-block-binding-reach.test.tsx 回答的是一个刻意窄的问题:声明了 objectName 的 public block,挂载后有没有一次数据调用带上这个对象名。它首跑抓到两个真缺陷(#3144,已由 #3147 修复),价值是实的。本 issue 记录它没有覆盖的部分,以及每一块的处置决定。

console 的 curated 契约共 57 个 block(public-contract.test.tsEXPECTED_COVERED)。当前行为覆盖 24(两道探针的并集)。

范围 状态
1 候选筛选被 lazy stub 缩水 #3153,8/14 → 14/14
2 已探块的非 objectName 绑定 ✅ 已落地,但原方案不存在,见下
3a record:* 家族(11 个,原文写 10) ✅ 已落地,8 个响应、3 个入台账,抓到 2 个缺陷
3b 纯展示原语(约 30 个) 决定不做,理由见下

第 1、2、3a 层已全部落地(#3153 / #3166);3b 是明确的不做。除 3b 外无剩余项。


1. 候选筛选被 lazy stub 静默缩水 ✅ 已由 #3153 修复

> 更正(开 issue 后核实):本条最初写的是"这批是 #4413 复发风险最高的一批"。那个判断是错的,记在这里免得下一个人照着它排优先级。逐个核过接线后:ObjectChart 自己读 context(ObjectChart.tsx:253props.dataSource || context?.dataSource,所以裸注册在它这里安全),object-gantt / object-timeline / object-map / object-kanban / object-calendar 都有 context→prop wrapper。六个块接线本来就是对的,没有第三个 #3144
>
> 本条的性质因此不是"6 个高风险块没被探到",而是门禁静默少报了自己的覆盖面——价值仍在,但是止损一个和 objectstack#4472 同族的谎,只不过这次是新门禁对自己的作用域撒谎。

探针用 getPublicConfigs().filter(有 objectName 输入) 选候选,而 console 对这些块走 registerLazy,pending stub 不带 inputsRegistry.getMeta 的注释写得明明白白:

> A stub carries only what registerLazy was given, so inputs is usually absent until the chunk loads. Consumers should treat that as "not yet known", not as "declares no props".

探针恰恰犯了这条注释警告的错。这是 #2953 的形状(lazy 注册悄悄掉出契约)在消费端复发。原守卫 length > 0 + toContain('object-form') 在 8/14 时两条都是真的,看不见缩水。

#3153 的修法:候选筛选前经注册表自己的 loadLazy 解析 pending 条目;守卫改成精确清单。


2. 已探块的非 objectName 绑定 ✅ 已落地——但原方案在代码库里不存在

> 更正(做的时候核实的):本条原来提议的切片是"recordIdobject-form / object-master-detail-form / embeddable-form 三个块上"。这三个块都没有声明 recordId 输入——事实上 57 个 public block 里没有任何一个声明 recordId 全仓库只有两处 recordId/resourceId 输入声明,都在 plugin-detail/src/index.tsxview:detaildetail-view),两个都不在 curated public 集合里。
>
> 这和 objectstack#4472 的方向 (d) 是同一类结果:一个从声明出发提出的切片,等真去看声明的时候发现它不在。记在这里,因为这正是这条线要说的事——从声明推断能力,推错的成本就是这样。

代之以落地的是同机制、同成本、但确实存在的切片:record:related_listrecord:line_items 各自把 requiredrelationshipField + childObject 绑进子查询,且只有在 record context 下才观察得到——也就是只有第 3a 层那道探针能看见。断言:两次 find() 都必须带上声明的子对象名和关系字段,并且 scope 到当前绑定的父记录 id。

结果:两个都成立,调用形如

find("probe_child__c", {"$filter":{"parent_probe_id":"probe-record-aaaa"},"$top":5,"$skip":0})

"同机制换断言"这个假设成立,而且成本是在本来就要发生的挂载上多加一条断言。原文担心的噪声比在这个切片上没有出现。

噪声比:更新后的账(这条更正会改变结论方向

> 本节先前写的是"2 个真缺陷 : 5 个探针自造假信号"。那是漏更新——假信号数加了第 3a 层新增的一个,真缺陷数却没跟着加。实际是:

数量 明细
真缺陷 4 list-view / embeddable-form 缺 dataSource 桥(#3144)、record:activity 恒空(#3165)、useMetadata 渲染死循环
探针自造假信号 5 Proxy 展开剥光方法、订阅方法返回 Promise 炸卸载、data 盖掉 objectName 绑定、object-map 的 maplibre teardown 抛错、sections 通用样本让 record:details

从 1:2 变成接近 1:1。原文"诊断尾巴不封顶、不建议全面铺开"的判断建立在前一个数字上,按新数字它站不住了:外扩的性价比比当初估计的好得多。

真正该保留的谨慎不是"别扩",而是扩的方式:每个假信号都是"给每个输入填了合理值 ≠ 配置整体合理"的一个实例,所以外扩要一次一个绑定、带负对照(第 3a 层那个"同一条记录挂两次必须逐字节相同"的仪器自检)地扩。有了负对照,假信号会当场暴露成仪器故障而不是伪装成发现。

3a. record:* 家族 ✅ 已落地(11 个,不是 10)

原文漏了 record:line_items。实际的 public record:* 是 11 个:details / highlights / related_list / path / line_items / activity / discussion / history / quick_actions / reference_rail / alert。(record:chatter 不是 public——它是 record:discussion 的同渲染器别名,public-contract.test.ts 按这个理由排除。)

探针apps/console/src/__tests__/record-block-record-reach.test.tsx。原文说"断言从有没有调 dataSource 变成渲染出的是记录内容还是 placeholder"——实际做成了差分,因为"是不是 placeholder"对 11 个块没有统一判据(path 渲染的是高亮哪一段、alert 渲染的是显不显示、history 渲染的是取数参数):

> 把 block 挂在 record context 下,绑两条不同的、同对象的记录,各挂一次。DOM 或数据调用有没有变化

几个刻意的设计决定,都是为了这道门禁不会变成它自己要消灭的东西:

  • 两条记录,而不是"绑 vs 不绑"。不绑的对照组树形不同(少一层 provider),React.useId 会整体偏移,于是每个块都"有差异"——探针自己制造绿。
  • 差分,而不是"渲染非空"。后者对一个无视全部输入的块也恒为真,正是 3b 那条不做的理由。
  • 仪器本身也被断言:每个块还会用记录 A 再挂一次,要求和第一次逐字节相同。这条一旦失败,"A 和 B 不同"就不再等于"记录到达了输出",整个文件的绿就是噪声——所以是断言,不是假设。
  • 崩溃按崩溃报。SchemaRenderer 会把渲染期抛错吃掉画成错误卡片,而崩了的块对两条记录渲染的是同一张卡片——不单独断言的话它会落进"无差异"那一档,被读成关于绑定的结论。(这条不是假想:sections 的样本值就把 record:details 送进了这个陷阱。)
  • 无外网。这家族里有块直接调 fetch('/api/v1/security/explain'),happy-dom 下相对路径会打到 localhost:3000——探针原本"能跑"只是因为连接被拒,每跑一次刷 24 行 ECONNREFUSED,而且谁本地起了 3000 端口结果就不一样。现在 fetch 立即 reject,URL 进同一份调用台账(走裸 fetch 绑定记录的块因此照样算数)。

结果:8 个响应绑定记录(details / highlights / related_list / path / line_items / history / quick_actions / alert),3 个入 NO_RECORD_REACH 台账,每条都必须写明宿主路径——因为"宿主供数"只有在真有宿主在供的时候才是理由:

block 台账理由 核得住吗
record:discussion items 来自 DiscussionContext,由 app-shell RecordDetailView ✅ 宿主路径是真的
record:reference_rail entriesbuildDefaultPageSchema 注入,不是作者输入 ✅ 宿主路径是真的
record:activity 渲染器把 items={[]} 写死,没有任何路径在供数 这就是 #4413,第三次

抓到的两个缺陷

  1. record:activity 的 feed 结构上不可能有内容——11 个声明的输入全是空 feed 上的过滤器(objectstack#4413 的形状,第三次) #3165record:activity 的 feed 结构上不可能有内容。 渲染器 useRecordContext() 调了就丢,items={[]} 写死;底层组件不取数;synth 发出的节点不带 props 且触发它的 showActivity 生产代码从没设过 true。11 个声明的输入全是这个恒空 feed 上的过滤器和开关。它自己的文件头还写着 "Real data wiring … lives inside that component"——不在,那一句就是缺陷本身。 单独开 issue,因为修法是个功能(镜像 record:history 已有的 sys_activity 自取数),不是 list-view / embeddable-form 在 SDUI 注册表路径上拿不到 dataSource——声明为 required 的 objectName 是死的(objectstack#4413 同形) #3144 那种一行桥接。
  2. useMetadata() 的"优雅兜底"是个无限渲染循环。每次调用都新建兜底对象,于是 provider 外每渲染一次 getItem 就换一个身份;useMetadataItemgetItem 放在 effect deps 里、并在无 name 分支 setState 一个全新对象 → effect 重跑 → 新 state 对象 → 重渲染 → 新身份,同步到能把 render() 卡住。注释里写它存在的理由是"让 provider 外的单测能正常渲染"——它恰恰让那些消费者根本挂不起来(record:alert / record:quick_actions 各自打满一个核、涨到 8.6 GB)。已由 test(console): record:* 家族第一次有了行为证据,抓到 record:activity 恒空与 useMetadata 的渲染死循环 (#3149) #3166 修掉:兜底改成冻结的模块级单例,并让清空分支在已清空时直接 bail。

3b. 纯展示原语(约 30 个)❌ 决定不做

flex / grid / card / text / image / badge / …,无数据绑定,唯一能断言的性质是"渲染非空 / 非 placeholder"。

这层不覆盖,理由是可断言的性质太弱:对一个吐空 div 的块,"渲染非空"永远为真。加上去会得到一道报绿但什么都没检查的门禁——正是 objectstack#4472 要消灭的东西,也正是"一道报绿的假门禁比没有门禁更危险"那句话的字面意思。与其加个橡皮图章让覆盖率数字好看,不如在这里显式记一句不做和为什么。

如果将来有人想覆盖这层,先要回答的是"对一个展示原语,什么性质值得断言"——在有答案之前,不覆盖是正确的状态,不是欠债。

(第 3a 层的差分设计其实给出了一条可能的路:变一个输入,看输出变不变。但那要为每个原语挑一个"该被读到"的输入,是设计工作,不是套模板——所以仍然不做,只是把线索留在这里。)


边界,免得这个 issue 又被过度解读

到达数据层 / 记录到达输出 ≠ 渲染正确;这条链上仍然没有任何环节看渲染结果(parity 门禁比声明、prop 门禁 parse JSX、契约从 schema 生成——ADR-0082 addendum 已写明)。本 issue 的范围是"绑定接没接上",不是视觉回归测试。

参考

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions