Skip to content

refactor(test): split metadata-references by family, off the 100KB ceiling (#814) - #930

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-814-split-metadata-references
Aug 6, 2026
Merged

yinlianghui merged 2 commits into
mainfrom
claude/issue-814-split-metadata-references

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #814

test/metadata-references.test.ts 按族拆成四个文件,把这个测试模块从 100KB hygiene 上限边上挪开。纯 test/ 重构,不动 src/、不动 content/、不动 @objectstack/* 版本。

前提复核(premise 有变,结论不变)

issue 正文写的是「余量 817 字节」,那是在它引用的 67bf9474 上量的。#815 合并后这个数字已经变了 —— 本 PR 的基线 origin/main @ 6014b2cf 上实测:

字节 占 MAX_FILE_BYTES
67bf9474(issue 引用) 101,583 99.2%(余 817)
6014b2cf(本 PR 基线) 99,872 97.5%(余 2,528)

scripts/check-source-hygiene.mjs 的 MAX_FILE_BYTES = 100 * 1024 = 102,400,未变。余量从 817 涨到 2,528 是因为 #815 已被迫把 bulk 派发那部分搬进 test/bulk-action-dispatch.test.ts。

2,528 字节仍然是临界的 —— 本仓一条带解释的守卫惯例上 1–3KB,也就是「还能塞下一条,塞两条必红」。issue 的核心论断(下一个改这个文件的 agent 会白跑一轮,且这是被动拆分而非主动按族拆)完全成立,只是那个具体数字已经过期。另核:#906 的 ROW_LABEL 改动不在此文件(落在 test/sharing-coverage.test.ts 与 test/automation-docs-coverage.test.ts),未消耗本文件预算。

拆分方案

边界依文件内实际的 22 个顶层 describe 族群定,不是照 issue 建议硬套;issue 建议的三个名字恰好对上其中三族,第四族(页面/表单/跨面)留在原文件。

文件 覆盖的 describe
test/metadata-references.test.ts(保留本体) page component references · formula fields as query predicates · flow conditions reach CEL · object references outside views · forms can author the data · page templates stay in record context · form views' dead data provider · app AI bindings
test/view-references.test.ts view field references · priority queues sort by urgency · filter template tokens · row colors and kanban groups · every canonical opportunity stage · every named list view is reachable
test/action-references.test.ts navigation reaches everything · dashboard actions land on real routes · list-level action references · dashboard date ranges
test/i18n-references.test.ts picklist values never reach the UI unresolved · action labels translated · select fields translated · every locale is complete

四个文件共用的派生量(objects / views / walk / 展平后的 locale packs / 平台对象白名单)提到 test/helpers/metadata-fixtures.ts(无断言、不以 .test.ts 结尾,vitest 不收集),四处 import,避免同一份派生抄四遍 —— 那份 locale-pack 展平逻辑历史上已经被写错过一次。

各族内部保持原顺序,因此所有「the guard above」「the two guards above」「The sibling guard above」的相邻关系原样成立(row colors → canonical stage、action labels → select fields、select fields → locale complete、navigation → dashboard actions)。

test/bulk-action-dispatch.test.ts 按裁定未动。

对账表

拆前单跑:Test Files 1 passed (1) / Tests 70 passed (70)。

文件 用例数 字节 距 100KB 上限余量
test/metadata-references.test.ts 24 30,578 70.1%
test/view-references.test.ts 16 21,158 79.3%
test/action-references.test.ts 11 18,983 81.5%
test/i18n-references.test.ts 19 33,451 67.3%
test/helpers/metadata-fixtures.ts —(无测试) 4,028 96.1%
合计 70

24 + 16 + 11 + 19 = 70,与拆前一致。每个产出文件余量均 ≥ 30%(最小 67.3%)。

注:70 而非静态 it( 行数 68 —— priority queues 族用的是 it.each(['crm_case','crm_task']),展开成 2 条。

语义零变化,且是机械验证的

不是「我看了一遍觉得没改」。校验脚本把四个文件里的 22 个 describe 体逐块与原文件对拍:

  • 18/22 块逐字节相同;
  • 另 4 块的差异全部来自下面这 5 处注释改写,把这 5 处反向替换回去后,22 块全部逐字节还原为原文;
  • 6 处原有的块前 JSDoc 全部逐字节相同;
  • test/helpers/metadata-fixtures.ts 与原 preamble 第 23–100 行逐字节相同(差异仅 export 前缀 + 下面第 6 处改写)。

被改写的 5 + 1 处注释,都是拆分本身让原句变成假话的句子,不是「顺手优化」:

# 原文 改后 为什么必须改
1 and moved: this file is one edit from the 100KB hygiene ceiling. 点名 test/metadata-references.test.ts 当时的处境,并注明 #814 已按族拆完 该 #815 指路注释随 list-level action 族搬到了 action-references.test.ts,「this file」指错了对象
2–5 the navigation guard in this file spent its life passing/green(4 处) 把 this file 换成点名 test/action-references.test.ts navigation 守卫搬走了,这 4 处分别落在 view-references(1)与 i18n-references(3)
6 preamble 里 localePacks 的 the navigation guard below 同上,改为点名文件 该注释随 localePacks 进了 helpers,已无「below」

除此之外:没有改任何断言、任何 helper 逻辑、任何 fixture、任何测试名。

指路注释

沿 #815 的既有样式(/** … */ 块,讲清「什么搬走了、搬去哪、为什么」):

这样处理的一个副作用是可控的:src/views/opportunity.view.ts:311、src/views/task.view.ts:174、src/dashboards/service.dashboard.ts:72 和 test/bulk-action-dispatch.test.ts:86 里那几条「见 metadata-references.test.ts 的某守卫」的注释,指向的守卫这次搬了家。因裁定不动 src/、不动 bulk-action-dispatch.test.ts,这几条现在需要多跳一次(落到 split map 再定位)。已另开 observation-class finding 记录,未在本 PR 顺手改。

六道门(均在 flock /tmp/os-heavy-verify.lock 内串行)

门 结果
pnpm typecheck exit 0
pnpm validate exit 0(13 条既有 approval/field-group 警告,与 main 同)
pnpm lint exit 0(13 warning(s), 14 suggestion(s),与 main 同)
pnpm hygiene 四项全绿,含 ✓ no source file over 100KB
pnpm build exit 0,Artifact: dist/objectstack.json (1921.4 KB)
pnpm test -- --maxWorkers=2 Test Files 69 passed (69) / Tests 1597 passed | 1 skipped (1598)

main 基线(同一 worktree、拆分前实测):Test Files 66 passed (66) / Tests 1597 passed | 1 skipped (1598)。用例总数两侧完全一致,文件数 66 → 69(+3)。

控制字节自扫(含 .changeset/):grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 全部无命中;本仓无 scripts/check-nul-bytes.mjs,由 hygiene 的 control-byte 项把关,已绿。

未起任何 dev server。


Generated by Claude Code

…iling (#814)

`test/metadata-references.test.ts` had reached 99,872 bytes against the
`MAX_FILE_BYTES = 100 * 1024` cap in `scripts/check-source-hygiene.mjs` —
97.5% of the quota, about one commented guard of headroom. #815 already paid
for that once: it set out to invert a single bulk-dispatch guard, blew the
cap, and had to design an unplanned split mid-PR before its real change could
be reviewed. Every later PR touching these guards was queued to repeat it.

The 22 describe blocks are re-homed by the metadata surface each resolves
against:

  metadata-references  pages, forms, and the cross-surface references
                       (formula predicates, flow conditions, objectOverride,
                       dashboard global filters, App.defaultAgent)   24 tests
  view-references      view fields, sorts, filter template tokens, row
                       colours, kanban groups, stage enumerations, list-view
                       reachability                                  16 tests
  action-references    navigation, dashboard actions and routes, list-level
                       rowAction / bulkAction, dashboard date ranges 11 tests
  i18n-references      action labels, select fields and options, locale
                       completeness on every authored surface        19 tests

Shared derivations (objects, views, walk, the flattened locale packs, the
platform-object allowlist) move to `test/helpers/metadata-fixtures.ts` — one
definition instead of four. `metadata-references.test.ts` keeps a map at the
top naming which family went where, so the existing comments in `src/` and in
`bulk-action-dispatch.test.ts` that point at this file still land on their
guard in one hop.

Semantics are unchanged and were verified mechanically: every describe body
was diffed against the original and is byte-identical, except five comment
sentences the split itself falsified ("the navigation guard in this file",
which now names `test/action-references.test.ts`). Reversing those five
restores the original bytes exactly. 24 + 16 + 11 + 19 = 70 tests, the same 70
the single file ran; the full suite is 1597 passed | 1 skipped on both sides.

Largest resulting file is 33,451 bytes — 67% below the ceiling.
@vercel

vercel Bot commented Aug 6, 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)
hotcrm Ignored Ignored Aug 6, 2026 9:19am

Request Review

Comment thread test/i18n-references.test.ts Fixed
Comment thread test/i18n-references.test.ts Fixed
Comment thread test/action-references.test.ts Fixed
Comment thread test/i18n-references.test.ts Fixed
Comment thread test/i18n-references.test.ts Fixed
Comment thread test/i18n-references.test.ts Fixed
Comment thread test/metadata-references.test.ts Fixed

Copy link
Copy Markdown
Collaborator Author

CI 对账(补一句,免得 reviewer 自己去凑数)

PR 正文里的 main 基线是在本 PR 的分叉点 6014b2cf 上实测的:Test Files 66 passed (66) / Tests 1597 passed | 1 skipped (1598)。

CI 跑的是与当前 main 的合并结果,而分叉之后 main 又进了 3 个 commit(#922 / #923 / #924),其中带进来一个新测试文件。所以 Build and Test (22.x) 报的是:

 Test Files  70 passed (70)
      Tests  1601 passed | 1 skipped (1602)

拆开对账:

文件数 用例数
main @ 6014b2cf(本 PR 基线) 66 1597
本 PR 拆分(1 → 4) +3 +0
main 后续 #923 带进的 test/docs-conversion-rate-spelling.test.ts +1 +4(CI 日志里 test/docs-conversion-rate-spelling.test.ts (4 tests))
CI 实测 70 1601

也就是说 本 PR 对用例总数的净贡献是 0,1597 → 1601 全部来自 main 自己的新测试。文件数 +3 是拆分的预期结果。

其余检查:Quality Checks(含 pnpm hygiene)、Check Changeset、link-check、Label Pull Request、Vercel Preview Comments 均 success;Playwright 与 Analyze Code (javascript) 在跑,二者都不受本次纯单测文件重构影响。

与 main 无冲突(git merge-tree 干净,main 后续三个 commit 未碰本 PR 改动的任何文件)。


Generated by Claude Code

CodeQL failed the PR with 7 alerts on the new files. Two distinct causes,
both real:

Three imports went dead in the move. `metadata-references` no longer needs
`profiles` (the preamble's only reader of it was `profileNames`, which now
lives in the shared fixtures module), and `action-references` /
`i18n-references` no longer need `walk` — every module-level `walk` call site
landed in `metadata-references` or `view-references`, and the two `walk`
identifiers still visible in those files are LOCAL shadows (a named function
expression at action-references.ts:52, a block-scoped const at
i18n-references.ts:636). They shadowed the module-level `walk` in the original
file too, so removing the imports changes no resolution.

Two `for` statements destructured a `key` binding they never read
(`for (const { key, objectName, fieldName } of selectFields)`). That is dead
in the original, byte-for-byte; CodeQL reports it as new only because the code
now sits at a new path. Fixed by dropping the unused binding — the `key`
property on `selectFields` is left alone, since removing it would be a change
to the derivation rather than to a dead reference.

Verified with `tsc --noEmit --noUnusedLocals --noUnusedParameters`, which
reproduces exactly CodeQL's five locations and now reports none. Test counts
are unmoved: 24 + 16 + 11 + 19 = 70, full suite 1597 passed | 1 skipped.

Copy link
Copy Markdown
Collaborator Author

CodeQL 红了一轮,已修(65ef07b4)

40b00d5c 上 CodeQL 报 7 条新 alert(含 2 条 error),全部落在拆出来的新文件上。是真问题,不是误报,但两类成因不同,分开说,因为它们对「拆分零语义变化」这条承诺的含义不一样。

先说结论:我用 tsc --noEmit --noUnusedLocals --noUnusedParameters 复现了 CodeQL 的判断——它精确命中同样 5 个位置,一个不多一个不少,这比逐个读 alert 更能确定我没漏:

test/action-references.test.ts(13,3):   error TS6133: 'walk' is declared but its value is never read.
test/i18n-references.test.ts(15,3):     error TS6133: 'walk' is declared but its value is never read.
test/i18n-references.test.ts(265,18):   error TS6133: 'key' is declared but its value is never read.
test/i18n-references.test.ts(285,18):   error TS6133: 'key' is declared but its value is never read.
test/metadata-references.test.ts(11,3): error TS6133: 'profiles' is declared but its value is never read.

修完后同一条命令对这 5 个文件零输出。

成因一:3 个 import 被我搬成了死引用(我的锅)

我当初是用正则扫每个 describe 块里出现的符号来定 import 列表的——正则会扫到注释里的散文,于是把只在散文里出现的名字也 import 了进来:

文件 死 import 为什么在原文件里是活的
metadata-references profiles 原 preamble 里唯一读它的是 profileNames = new Set(profiles.map(...)),而 profileNames 这次进了 test/helpers/metadata-fixtures.ts。文件里剩下的 profiles 只出现在测试名字符串 'assignedProfiles name real profiles' 和一句散文里
action-references walk 模块级 walk 的调用点全部落在 metadata-references 与 view-references 两族
i18n-references walk 同上

walk 这条要特别说一句,因为这两个文件里 walk 这个词其实还在,只是那是局部遮蔽(shadow),不是 import:

  • action-references.ts:52 —— flatMap(function walk(n: AnyRec): AnyRec[] { ... .flatMap(walk) }),具名函数表达式的名字只绑定在自己体内;
  • i18n-references.ts:636 —— const walk = (node, bound?) => {...},块级 const。

这两处在原文件里同样遮蔽着模块级 walk(原文件 :499 / :1210 / :2064),所以删掉 import 不改变任何一处名字解析。view-references 里的 walk import 是活的(:202 / :323 真在调模块级的那个),CodeQL 没报它,对。

成因二:2 处 key 是原文件里就有的死绑定(不是我引入的)

for (const { key, objectName, fieldName } of selectFields) {          // key 从未被读
for (const { key, objectName, fieldName, values } of selectFields) {  // 同上

这两行在 origin/main 的 test/metadata-references.test.ts:1583 / :1603 上逐字节相同,key 在两个 it 体里都没被读过——而那两个 it 体本身是逐字节搬过来的。CodeQL 把它报成 new alert,只是因为同一段代码换了文件路径。

修法取最小:只删掉那个没人读的解构绑定,selectFields 上的 key: 属性本身不动——删属性属于改派生逻辑,那才是真的越界。

-    for (const { key, objectName, fieldName } of selectFields) {
+    for (const { objectName, fieldName } of selectFields) {

(顺带记录一个故意不改的点:key: ${obj.name}.${fieldName} 这个属性现在全文件无人读取,是死属性。CodeQL 不报它——它查未使用变量,不查未使用属性。我没顺手删,理由同上:它落在 selectFields 的派生里,不在「删死引用」的范围内。留给后续想收拾的人,或并入 #931。)

对「零语义变化」承诺的影响:从 5 处变成 5 + 2 处

PR 正文里那张「被改写的注释」表现在要多算两行代码。重跑对拍脚本,把 5 处注释改写 + 2 处解构绑定删除一起反向替换回去:

describe bodies restored byte-identically after reversing the 5+2 edits: 22 / 22
byte-identical with NO reversal:                                         18 / 22

即:22 个 describe 体里 18 个仍逐字节相同,另 4 个的全部差异就是这 7 处,反向替换后 22/22 完全还原。断言、helper 逻辑、fixture、测试名依旧一处未动。

重跑六道门

门 结果
pnpm typecheck exit 0
pnpm validate exit 0
pnpm lint 13 warning(s), 14 suggestion(s),与 main 同
pnpm hygiene 四项全绿,含 ✓ no source file over 100KB
pnpm build exit 0,Artifact: dist/objectstack.json (1921.4 KB)
pnpm test -- --maxWorkers=2 Test Files 69 passed (69) / Tests 1597 passed | 1 skipped (1598)

对账数字一个没动:24 + 16 + 11 + 19 = 70,与拆前单跑的 70 一致;全量与 main @ 6014b2cf 基线的 1597 passed / 1 skipped 一致。

字节数微调(删了 3 行 import + 2 处 key, ):

文件 字节 余量
test/metadata-references.test.ts 30,566 70.2%
test/view-references.test.ts 21,158 79.3%
test/action-references.test.ts 18,975 81.5%
test/i18n-references.test.ts 33,433 67.4%
test/helpers/metadata-fixtures.ts 4,028 96.1%

控制字节自扫仍全部无命中。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

全绿(65ef07b4),并把上一条对账里的数字校准一下

9 项检查全部 success,含 CodeQL: success(上一轮在 40b00d5c 是 failure):

CodeQL · Build and Test (22.x) · Quality Checks · Check Changeset · Analyze Code (javascript) · Playwright · link-check · Label Pull Request · Vercel Preview Comments

数字校准:1601 → 1602,仍与本 PR 无关

我上一条对账写的是 CI 报 1601。65ef07b4 这轮报的是 1602,多的那 1 条不是我加的——两轮 CI 之间 main 又进了一个 commit:

4d743032 test(docs-drift): make the tile prose rule read every locale (#725) (#929)

它改的是 test/docs-drift.test.ts(+1 用例)。完整账:

文件数 用例数
main @ 6014b2cf(本 PR 分叉点) 66 1597
本 PR 拆分(1 → 4) +3 +0
main #923 带进的 test/docs-conversion-rate-spelling.test.ts +1 +4
main #929 改的 test/docs-drift.test.ts +0 +1
CI @ 65ef07b4 实测 70 1602

本 PR 对用例总数的净贡献依旧是 0;1597 → 1602 全部来自 main 自己的 5 条新用例。本地在分叉点的树上跑仍是 Tests 1597 passed | 1 skipped,四个文件 24 + 16 + 11 + 19 = 70。

与 main 仍无冲突(git merge-tree 0 个冲突标记;main 后续 4 个 commit 未碰本 PR 改动的任何文件)。


Generated by Claude Code

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

Labels

ci/cd CI plumbing and the verification pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test/metadata-references.test.ts 距 100KB hygiene 上限只剩 817 字节 —— 下一个往里加测试的 PR 会被 gate 拦住

3 participants