Repository navigation
refactor(test): split metadata-references by family, off the 100KB ceiling (#814) - #930
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
CI 对账(补一句,免得 reviewer 自己去凑数)PR 正文里的 main 基线是在本 PR 的分叉点 CI 跑的是与当前 main 的合并结果,而分叉之后 main 又进了 3 个 commit(#922 / #923 / #924),其中带进来一个新测试文件。所以 拆开对账:
也就是说 本 PR 对用例总数的净贡献是 0,1597 → 1601 全部来自 main 自己的新测试。文件数 +3 是拆分的预期结果。 其余检查: 与 main 无冲突( 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.
CodeQL 红了一轮,已修(
|
| 文件 | 死 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
全绿(
|
| 文件数 | 用例数 | |
|---|---|---|
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
Fixes #814
test/metadata-references.test.ts按族拆成四个文件,把这个测试模块从 100KB hygiene 上限边上挪开。纯 test/ 重构,不动src/、不动content/、不动@objectstack/*版本。前提复核(premise 有变,结论不变)
issue 正文写的是「余量 817 字节」,那是在它引用的
67bf9474上量的。#815 合并后这个数字已经变了 —— 本 PR 的基线origin/main@6014b2cf上实测:MAX_FILE_BYTES67bf9474(issue 引用)6014b2cf(本 PR 基线)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 建议的三个名字恰好对上其中三族,第四族(页面/表单/跨面)留在原文件。test/metadata-references.test.ts(保留本体)test/view-references.test.tstest/action-references.test.tstest/i18n-references.test.ts四个文件共用的派生量(
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)。test/metadata-references.test.tstest/view-references.test.tstest/action-references.test.tstest/i18n-references.test.tstest/helpers/metadata-fixtures.ts24 + 16 + 11 + 19 = 70,与拆前一致。每个产出文件余量均 ≥ 30%(最小 67.3%)。
注:70 而非静态
it(行数 68 ——priority queues族用的是it.each(['crm_case','crm_task']),展开成 2 条。语义零变化,且是机械验证的
不是「我看了一遍觉得没改」。校验脚本把四个文件里的 22 个
describe体逐块与原文件对拍:test/helpers/metadata-fixtures.ts与原 preamble 第 23–100 行逐字节相同(差异仅export前缀 + 下面第 6 处改写)。被改写的 5 + 1 处注释,都是拆分本身让原句变成假话的句子,不是「顺手优化」:
and moved: this file is one edit from the 100KB hygiene ceiling.test/metadata-references.test.ts当时的处境,并注明 #814 已按族拆完action-references.test.ts,「this file」指错了对象the navigation guard in this file spent its life passing/green(4 处)this file换成点名test/action-references.test.tsthe navigation guard below除此之外:没有改任何断言、任何 helper 逻辑、任何 fixture、任何测试名。
指路注释
沿 #815 的既有样式(
/** … */块,讲清「什么搬走了、搬去哪、为什么」):metadata-references.test.ts顶部 JSDoc 追加一节SPLIT BY FAMILY (#814),逐族列出三个兄弟文件各自持有的 describe 全名,外加 fix(opportunity): 用声明通道params._selectedIds接回多选批量改阶段 (#508) #815 拆出的bulk-action-dispatch.test.ts;这样处理的一个副作用是可控的:
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 typecheckpnpm validatepnpm lint13 warning(s), 14 suggestion(s),与 main 同)pnpm hygiene✓ no source file over 100KBpnpm buildArtifact: dist/objectstack.json (1921.4 KB)pnpm test -- --maxWorkers=2Test 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