Skip to content

test(console): record:* 家族第一次有了行为证据,抓到 record:activity 恒空与 useMetadata 的渲染死循环 (#3149) - #3166

Merged
os-zhuang merged 1 commit into
mainfrom
claude/react-blocks-conformance-9cjs1t
Aug 1, 2026
Merged

test(console): record:* 家族第一次有了行为证据,抓到 record:activity 恒空与 useMetadata 的渲染死循环 (#3149)#3166
os-zhuang merged 1 commit into
mainfrom
claude/react-blocks-conformance-9cjs1t

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

#3149 的第 2 层和第 3a 层。第 1 层是 #3153,第 3b 层是明确的不做。除 3b 外无剩余项,本 PR 合并后 #3149 可关。

为什么需要第二道探针

#3146public-block-binding-reach.test.tsx 问的是:声明了 objectName 的 block 裸挂载后,有没有一次数据调用带上这个对象名。record:* 家族按构造在这个问题之外——它们从 RecordContextProvider 取数,裸挂载正确地什么都不做,于是"没有数据调用"什么也不说明。

而那恰好是 objectstack#4413 待过的地方:record:details / record:highlights / record:path / record:related_list 声明了没人读的 objectName+recordId,四个 block 在真实记录页上渲染空白,全程门禁皆绿——因为框架侧那道门禁比的是两份声明(objectstack#4472),而 binding-reach 探针根本看不见这个家族。在本 PR 之前,"这家族在记录页下能工作"是个没有任何行为证据的假设。

探针问什么

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

两道探针的行为覆盖 14 → 24 / 57(record:related_list 两边都在:binding-reach 把它记成"没有父记录就不该取数",本探针是第一个真让它取上数的地方——那条台账理由从声明变成了核过的事实)。

几个刻意的设计决定

这道门禁很容易写成它自己要消灭的东西,所以:

  • 两条记录,而不是"绑 vs 不绑"。 不绑的对照组树形不同(少一层 provider),React.useId 整体偏移,于是每个块都"有差异"——探针自己制造绿。
  • 差分,而不是"渲染非空"。 后者对一个无视全部输入的块也恒为真,这正是 record:* 家族与非 objectName 绑定的行为证据——binding-reach 覆盖 14→24/57,#4413 的原始形态不再是假设 #3149 记下覆盖展示原语的理由。在这里加一道同样的东西是自毁。
  • 仪器本身也被断言。 每个块还会用记录 A 再挂一次,要求和第一次逐字节相同。这条一旦失败,"A 和 B 不同"就不再等于"记录到达了输出",整个文件的绿就是噪声——所以是断言,不是假设。
  • 崩溃按崩溃报。 SchemaRenderer 会把渲染期抛错吃掉画成错误卡片,而崩了的块对两条记录渲染的是同一张卡片——不单独断言就会落进"无差异"档,被读成关于绑定的结论。这不是假想:sections 的通用样本值把 record:details 送进过这个陷阱。
  • 无外网。 这家族里有块直接调 fetch('/api/v1/security/explain'),happy-dom 下相对路径打到 localhost:3000——探针原本"能跑"只是因为连接被拒,每跑刷 24 行 ECONNREFUSED,谁本地起了 3000 端口结果就不一样。现在立即 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={[]} 写死,没有任何路径在供数 #3165

抓到的两个缺陷

1. record:activity 的 feed 结构上不可能有内容 → #3165(本 PR 只记台账,不修)

  • 渲染器 useRecordContext() 调了就丢,<RecordActivityTimeline items={[] as any}> 写死;
  • 底层组件的 items 是 prop,全文件没有任何取数;
  • buildDefaultPageSchema:614 发出的节点不带 props,而触发它的 options.showActivity 在生产代码里从没被设成 true(每个调用点传的都是 { slots } 或什么都不传);
  • 注册处声明了 11 个输入,全是这个恒空 feed 上的过滤器和开关。

它自己的文件头写着 "Real data wiring (sys_activity / sys_comment polling) lives inside that component"——不在。那一句就是缺陷本身。

不在本 PR 修,是因为修法是个功能(镜像同目录 record:history 已有的 sys_activity 自取数),不是 #3144 那种一行桥接。台账是双向断言的:它一旦开始响应记录,测试就会失败直到条目被删,所以销账不会忘。

2. useMetadata() 的"优雅兜底"是个无限渲染循环 → 本 PR 修掉

每次调用都新建兜底对象,于是 provider 外每渲染一次 getItem 就换一个身份;useMetadataItemgetItem 放在 effect deps 里、并在无 name 分支 setState 一个全新对象 → effect 重跑 → 新 state 对象 → 重渲染 → 新身份,同步到能把 render() 卡住

注释里写它存在的理由是"让 provider 外的单测能正常渲染"——它恰恰让那些消费者根本挂不起来。record:alertrecord:quick_actions 都无条件调 useMetadataItem,各自打满一个核、涨到 8.6 GB 而一条断言都没跑到。

两处改动,一处治本一处兜底:兜底对象改成冻结的模块级单例(身份稳定);清空分支在已清空时直接 bail,覆盖同一个循环换条路来的情况——任何每渲染重建 context value 的调用方,而这个接口的注释是明确邀请这种写法的("hand-rolled context values in tests keep working")。

第 2 层:原方案不存在,落地的是另一个

原提议是"recordIdobject-form / object-master-detail-form / embeddable-form 三个块上"。这三个块都没声明 recordId——57 个 public block 里没有任何一个声明。 全仓库只有 view:detaildetail-view 两处 recordId/resourceId 输入,都不 public。这和 objectstack#4472 方向 (d) 是同一类结果:一个从声明出发提的切片,等真去看声明的时候发现它不在。

代之以同机制、同成本、但确实存在的切片:record:related_listrecord:line_items 各自把 requiredrelationshipField + childObject 绑进子查询,且只有在 record context 下才观察得到。断言两次 find() 都带上声明的子对象名和关系字段,并 scope 到当前绑定的父记录 id。两个都成立

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

"同机制换断言"这个假设成立,成本是在本来就要发生的挂载上多加一条断言。是否继续外扩仍建议谨慎——噪声比现在是 2 个真缺陷 : 5 个探针自造假信号(第 3a 层新贡献了 sections 那个崩溃)。

边界

到达数据层 / 记录到达输出 ≠ 渲染正确。这条链上仍然没有任何环节看渲染结果(ADR-0082 addendum 已写明)。本 PR 的范围是"绑定接没接上"。

规格侧不动:ADR-0082 D1(spec 是协议)和 #3729 已经定过——RecordActivityProps 声明 11 个属性是协议正确,运行时跟不上是运行时的欠账,不是把协议削平的理由。

验证

  • record-block-record-reach.test.tsx:13 passed,~27s,0 次外网连接
  • 模拟回归:删掉 record:activity 的台账条目 → 测试失败并指名 objectstack#4413 的形状;说明这道门禁真的在检查东西
  • 本地全量 vitest run(AGENTS.md [WIP] Enhance every detail of the designer #10,已在 origin/main a889e31 上重跑):800 个文件 / 9327 passed / 29 skipped,exit 0
  • CI 全绿:4 个 test shard、Type Check、Lint、Build & E2E、Bundle Analysis、Build Docs、Dev-server fixture build

变更

文件
apps/console/src/__tests__/record-block-record-reach.test.tsx 新增,第 3a 层探针 + 第 2 层切片
packages/react/src/context/AppShellContext.tsx useMetadata() 兜底冻结成单例;useMetadataItem 清空分支 bail
.changeset/record-block-record-reach.md / .changeset/metadata-fallback-render-loop.md

Closes #3149

…a 的渲染死循环 (#3149)

#3146 的 binding-reach 探针按设计看不见 record:* 家族——它们从
RecordContextProvider 取数,裸挂载什么都不做,"没有数据调用"因此不说明任何
问题。而那恰好是 objectstack#4413 待过的地方:四个 block 声明了没人读的
objectName/recordId,在真实记录页上渲染空白,全程门禁皆绿。

record-block-record-reach.test.tsx 把 11 个 public record:* block 挂在
record context 下,绑两条不同的同对象记录各挂一次,问 DOM 或数据调用有没有
变化。两道探针的行为覆盖 14 → 24 / 57。

几个刻意的设计决定:

- 两条记录而不是"绑 vs 不绑":不绑的对照组少一层 provider,useId 整体偏移,
  于是每个块都"有差异"——探针自己制造绿。
- 差分而不是"渲染非空":后者对无视全部输入的块也恒为真,正是 #3149 记下
  不覆盖展示原语的理由;在这里加一道同样的东西是自毁。
- 仪器本身被断言:每个块用记录 A 再挂一次,要求逐字节相同。这条一旦失败,
  "A 和 B 不同"就不再等于"记录到达了输出",整个文件的绿就是噪声。
- 崩溃按崩溃报:SchemaRenderer 会把渲染期抛错画成错误卡片,而崩了的块对两条
  记录渲染同一张卡片,不单独断言就会落进"无差异"档、被读成关于绑定的结论。
- 无外网:这家族有块直接调 fetch('/api/v1/security/explain'),happy-dom 下
  打到 localhost:3000——原本"能跑"只是因为连接被拒。现在立即 reject,URL 进
  同一份调用台账。

结果 8 个响应、3 个入 NO_RECORD_REACH 台账。台账条目必须写明宿主路径,因为
"宿主供数"只在真有宿主供数时才是理由:record:discussion(DiscussionContext,
RecordDetailView 挂)和 record:reference_rail(entries 由
buildDefaultPageSchema 注入)都核得住,record:activity 核不住——见 #3165。

顺带修掉一个探针挂不起来的缺陷:useMetadata() 的"优雅兜底"每次调用都新建对象,
provider 外每渲染一次 getItem 就换身份;useMetadataItem 把它放在 effect deps
里并 setState 一个全新对象 → 无限循环,同步到能把 render() 卡住。注释说它存在
是为了让 provider 外的单测能渲染,它恰恰让那些消费者挂不起来。改成冻结的模块级
单例,并让清空分支在已清空时 bail。

#3149 第 2 层同时落地,但原方案不存在:57 个 public block 没有任何一个声明
recordId 输入(全仓库仅 view:detail / detail-view 两处,都不 public)——和
objectstack#4472 方向 (d) 同一类结果。代之以 record:related_list /
record:line_items 的 required relationshipField + childObject 必须进子查询、
且 scope 到绑定的父记录,两个都成立。

Closes #3149

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3cP1eY1novcNhQEDBrSZD
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 1, 2026 1:56pm

Request Review

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-3yFBSEjf.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.45KB 3.08KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 475.95KB 104.41KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.44KB 10.66KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.53KB 17.12KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.86KB 28.81KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 224.52KB 54.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 184.69KB 48.91KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.82KB 25.30KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.47KB 10.57KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 14:06
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit ea96284 Aug 1, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/react-blocks-conformance-9cjs1t branch August 1, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants