Plate 仓库测试双车道模型:以文件名后缀收敛 fast/slow 测试车道的基础设施清理实践
2026/9/15 13:38:52 网站建设 项目流程

Plate 仓库测试双车道模型:以文件名后缀收敛 fast/slow 测试车道的基础设施清理实践

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

Plate 是一个基于 Slate 构建、面向 AI 场景与 shadcn/ui 的富文本编辑器 monorepo,其测试体系横跨apps/packages/数十个包。本文以 docs/plans/2026-03-23-two-test-lanes-cleanup.md 这份清理计划为骨架,深入讲解 Plate 如何将测试模型收敛为「fast / slow 两条车道」的单一命名约定,并结合仓库中的运行器源码、配置常量与实际.slow.*测试分布,说明这套测试基础设施的运作原理与落地方式。读完本文,你将掌握 Plate 测试车道的完整心智模型:文件后缀如何决定测试归属、两条车道的运行器差异、性能门槛如何强制维护车道纪律,以及如何在自己的测试体系中复刻这套「命名即分类」的设计。

清理背景:为什么要把测试收敛为两条车道

大型 monorepo 的测试体系很容易在演进中积累隐式分类:除了显式的「慢测试」之外,工具链常常还会按照目录路径悄悄地把一部分测试划入慢速通道。Plate 的这次清理正是针对这一问题。

原计划文档明确指出当前状态的核心矛盾:

The current setup has a hidden path-bucketed slow lane intest-fast.mjson top of the explicit*.slow.*lane. That makesdocxand package-integration tests behave like slow tests without actually being named or run like slow tests.

即当时的test-fast.mjs运行器中存在一套基于路径分桶(path-bucketed)的隐藏慢速车道,它与显式的*.slow.*命名车道叠加在一起。结果是:docx与 package-integration 相关测试在行为上像慢测试(会被排除出快速通道),但在命名上、运行方式上又不是慢测试。这种「行为与命名不一致」的状态会带来一系列问题:

  • 可发现性差:开发者无法通过文件名判断一个测试属于快车道还是慢车道;
  • 工具行为不透明:同一份测试代码,是否被运行、被哪个运行器运行,取决于它所在的目录,而不是它自身的命名;
  • 重构风险高:移动文件位置可能导致测试在快慢车道之间意外漂移;
  • 维护负担重:路径分桶清单需要随着新测试的加入持续手工维护。

清理的目标因此非常明确——把测试归属的判定收敛为唯一、可见、与位置无关的规则:文件后缀。

目标模型:两条车道,一种命名约定

计划文档定义了清理后的最终测试模型,恰好两条车道:

车道命名约定说明
fast(快速车道)*.spec.ts[x]常规单元/组件测试,要求快速稳定
slow(慢速车道)*.slow.ts[x]重量级、IO 密集、集成类测试

这一模型的核心设计哲学是**「命名即分类」(naming is the classification)**:测试属于哪条车道,完全由文件名后缀决定,与文件所在目录无关。任何开发者只要看到foo.slow.tsx,就知道它会进入慢速车道;看到bar.spec.tsx,就知道它属于快速车道。这份计划沉淀在 docs/plans/2026-03-23-two-test-lanes-cleanup.md 中,而它的落地证据遍布仓库的配置与脚本中。

现状剖析:test-suites.mjs 中的模式定义

理解这次清理,首先要看测试车道的「事实来源」——tooling/config/test-suites.mjs。该文件集中定义了所有测试文件的 glob 匹配模式,是test-fast.mjstest-slow.mjs等运行器的唯一数据源:

export const TEST_FILE_PATTERNS = [ 'apps/**/*.spec.{ts,tsx}', 'packages/**/*.spec.{ts,tsx}', 'tooling/scripts/**/*.test.mjs', ]; export const TEST_SLOW_FILE_PATTERNS = [ 'apps/**/*.slow.{ts,tsx}', 'packages/**/*.slow.{ts,tsx}', ]; export const TEST_DEFERRED_FILE_PATTERNS = [ 'apps/**/__deferred__/**/*.deferred.spec.{ts,tsx}', ]; export const TEST_IGNORE_PATTERNS = [ '**/coverage/**', '**/dist/**', '**/node_modules/**', '.next/**', '**/__deferred__/**', ];

逐项解读这份配置:

  • TEST_FILE_PATTERNS:快速车道收集模式,覆盖apps/**packages/**下所有*.spec.{ts,tsx},外加tooling/scripts/**下的*.test.mjs(工具脚本自身的测试也进快车道);
  • TEST_SLOW_FILE_PATTERNS:慢速车道收集模式,同样是apps/**packages/**全域,但只看*.slow.{ts,tsx}后缀——注意这里没有任何路径分桶,慢车道完全由后缀驱动,这正是清理后想达到的状态;
  • TEST_DEFERRED_FILE_PATTERNS:延期测试(deferred)模式,位于__deferred__目录下的.deferred.spec.*,由独立的test:deferred车道处理,与 fast/slow 双车道模型分离;
  • TEST_IGNORE_PATTERNS:所有车道共同忽略的产物目录(coverage、dist、node_modules、.next)以及__deferred__目录本身,防止构建产物或延期测试被误收集。

从当前这份配置可以看出,路径分桶已经不存在——TEST_SLOW_FILE_PATTERNS只依赖文件名后缀,没有任何针对docxdocx-iopackage-integration目录的排除/包含规则。这印证了清理计划第 1 步(Remove path-based slow buckets fromtooling/config/test-suites.mjs)与第 2 步(fast-lane 工具停止按路径排除)已经落地。

清理执行:五个工作项的落地路径

计划文档将清理工作拆为五个可执行步骤,逐一对照仓库现状可以还原其落地轨迹:

第 1 步:从 test-suites.mjs 移除基于路径的慢速分桶。如上文所示,tooling/config/test-suites.mjs 当前的慢车道模式只认*.slow.{ts,tsx}后缀,路径分桶规则已完全移除。

第 2 步:更新 fast-lane 工具,停止按路径排除文件。test-fast.mjs 中收集快车道测试的核心逻辑为:

const allFastFiles = globSync(TEST_FILE_PATTERNS, { cwd: process.cwd(), ignore: TEST_IGNORE_PATTERNS, onlyFiles: true, }).sort();

它仅依据TEST_FILE_PATTERNS+TEST_IGNORE_PATTERNS做收集,文件路径只参与用户指定的过滤参数(pathFilters),不再内置任何「把某目录排除出快车道」的硬编码逻辑。

第 3 步:将当前被分桶的慢速 specs 重命名为*.slow.ts[x]计划明确列出四类需要重命名的测试:

  • packages/docx/src/**
  • packages/docx-io/src/**
  • apps/www/src/__tests__/package-integration/**
  • 当时的reactHeavyspecs

从当前仓库的慢车道文件分布(见下文第五节)可以看到,这四类测试如今全部以.slow.ts[x]后缀命名,例如packages/docx/src/lib/docx-cleaner/cleanDocx.slow.tspackages/docx-io/src/lib/__tests__/tables.slow.tsxapps/www/src/__tests__/package-integration/slate-contracts.slow.tsx。此外,全仓库搜索reactHeavy仅命中计划文档自身,说明该历史分类标记已无代码残留,重命名工作已完整收尾。

第 4 步:test:slow保持为唯一的慢速车道运行器。根目录 package.json 的脚本定义印证了这一点:test:slow指向bun tooling/scripts/test-slow.mjs,是慢速车道唯一的入口。

第 5 步:更新活跃测试指引,使「slow」仅指文件名后缀。这一语义已经固化在配置与脚本中:慢 = 文件名以.slow.ts[x]结尾,与目录位置、运行器内部逻辑无关。相关的同批次计划如 docs/plans/2026-03-23-docx-fast-lane-reclaim.md 也从 docx 一侧记录了快车道回收的配套工作。

运行器源码剖析:test-fast.mjs 与 test-slow.mjs

两条车道的运行器 test-fast.mjs 与 test-slow.mjs 共享同一套实现骨架,差异集中在测试收集模式与少量运行细节上。二者都基于 Bun 的bun test子进程驱动。

参数解析与路径过滤

两个脚本都定义了VALUE_FLAGS集合,把需要取值的 flag(如--bail--reporter--reporter-outfile--max-concurrency--seed--timeout-t等)与普通布尔 flag 区分开,剩余的非-前缀参数则进入pathFilters

for (let i = 0; i < rawArgs.length; i++) { const arg = rawArgs[i]; if (arg === '--') continue; const matchedValueFlag = [...VALUE_FLAGS].find( (flag) => arg === flag || arg.startsWith(`${flag}=`) ); if (matchedValueFlag) { bunArgs.push(arg); if (arg === matchedValueFlag) { const nextArg = rawArgs[i + 1]; if (nextArg) { bunArgs.push(nextArg); i += 1; } } continue; } if (arg.startsWith('-')) { bunArgs.push(arg); continue; } pathFilters.push(arg); }

pathFilters支持静态路径(目录或文件)与动态 glob(通过isDynamicPattern判断),最终从全量测试文件集合中筛选出选中文件。test-fast.mjs在未匹配到任何测试时会提示用户改用pnpm test:slow运行*.slow.*车道,形成两条车道的引导闭环:

if (selectedFiles.length === 0) { console.error( 'No fast-suite tests matched. Use `pnpm test:slow` for `*.slow.*` lanes.' ); process.exit(1); }

mock.module 隔离机制

两个运行器都实现了递归的 mock 依赖检测:脚本扫描测试文件源码中是否出现mock.module(字样,若没有则递归解析其本地相对导入(通过LOCAL_IMPORT_PATTERN正则 +LOCAL_SOURCE_EXTENSIONS扩展名候选列表解析真实文件),确认是否存在间接使用 mock 的测试:

const MOCK_MODULE_PATTERN = 'mock.module('; const LOCAL_IMPORT_PATTERN = /\b(?:import|export)\b[^'"]*?from\s*'"['"]|\bimport\s*\(\s*'"['"]\s*\)|\brequire\s*\(\s*'"['"]\s*\)/g;

凡检测到使用mock.module(的文件会被放入isolatedFiles单独逐个运行(避免模块 mock 污染同批次的共享执行环境),其余文件放入sharedFiles合并运行。这一隔离策略对两条车道一视同仁,保证了 mock 测试的确定性。

JUnit 报告合并

当以--reporter=junit --reporter-outfile=<file>运行时,脚本为每个批次生成独立的 JUnit XML(临时目录前缀分别为plate-fast-junit-plate-slow-junit-),随后通过剥离 XML 声明与<testsuites>包裹层、再统一合并的方式生成最终报告:

const mergeJunitReports = (files, outfile) => { const suites = files .map((file) => readFileSync(file, 'utf8') .replace(XML_DECLARATION_RE, '') .replace(TESTSUITES_OPEN_RE, '') .replace(TESTSUITES_CLOSE_RE, '') .trim()) .filter(Boolean); writeFileSync(outfile, [ '<?xml version="1.0" encoding="UTF-8"?>', '<testsuites name="bun test">', ...suites, '</testsuites>', '', ].join('\n')); };

两套运行器的差异

  • 收集模式不同test-fast.mjsTEST_FILE_PATTERNS*.spec.{ts,tsx}+tooling/scripts/**/*.test.mjs),test-slow.mjsTEST_SLOW_FILE_PATTERNS*.slow.{ts,tsx});
  • 文件路径显式化test-slow.mjs在最终运行前会为每个文件补./前缀(explicitPaths),确保 Bun 按显式路径执行慢速测试;
  • 空匹配提示不同:fast 车道提示转向test:slow,slow 车道则直接提示No slow tests matched.

性能门槛与车道纪律:test-slowest.mjs

双车道模型要真正成立,还必须防止快车道悄悄变慢。为此仓库在 tooling/config/test-suites.mjs 中定义了 CI 感知的性能阈值常量:

export const FAST_TEST_SLOW_CASE_THRESHOLD_MS = isCI ? 90 : 75; export const FAST_TEST_SLOW_FILE_THRESHOLD_MS = isCI ? 180 : 150; export const FAST_TEST_WARN_CASE_THRESHOLD_MS = isCI ? 75 : 60; export const FAST_TEST_WARN_FILE_THRESHOLD_MS = isCI ? 150 : 120;

阈值的语义是:

  • slow 阈值:单个用例超过 90ms(CI)/ 75ms(本地)、单个文件超过 180ms(CI)/ 150ms(本地),即判定为「应移入慢车道」的硬门槛;
  • warn 阈值:低于 hard 线但已偏慢(用例 75/60ms,文件 150/120ms)的警告区,CI 中作为可见的漂移信号;
  • CI 运行器噪声更大,因此 CI 侧阈值比本地略宽,本地保持更严格的纪律。

这套阈值由 test-slowest.mjs 执行:它内部调用test-fast.mjs并注入--reporter=junit,解析快车道每个用例/文件的耗时,与阈值比对后输出慢速报告。其配套参数包括:

  • --profile:非失败模式的分析运行(对应pnpm test:profile);
  • --top N:只输出耗时 Top N 的文件。

test-slowest.mjs顶部的注释写明了车道纪律的执行规则:

When a fast-suite spec repeatedly crosses these thresholds, move the whole spec to*.slow.ts[x].pnpm test:slowestandpnpm checkenforce these limits.

即:当快车道中的某个 spec 反复越过阈值,应把整个 spec 移到*.slow.ts[x]——不是只修单个用例,而是整体降级,这正是保持双车道纯净的机制。

仓库中实际的 slow 测试分布

清理完成后,慢车道测试在仓库中的分布完全遵循*.slow.{ts,tsx}后缀约定。当前仓库共有 77 个.slow.*测试文件,覆盖多个包与目录,代表性样本包括:

  • docx 包packages/docx/src/lib/docx-cleaner/cleanDocx.slow.ts(docx 清理器的重量级测试);
  • docx-io 包packages/docx-io/src/lib/__tests__/tables.slow.tsxpackages/docx-io/src/lib/__tests__/block_quotes.slow.tsxpackages/docx-io/src/lib/internal/html-to-docx.slow.ts等;
  • package-integration(apps/www)apps/www/src/__tests__/package-integration/slate-contracts.slow.tsxapps/www/src/__tests__/package-integration/docx/align.slow.tsxapps/www/src/__tests__/package-integration/core-static-html/serialize-html.roundtrip.slow.tsapps/www/src/__tests__/package-integration/markdown-deserializer/deserializeMd.slow.tsx等;
  • AI 包packages/ai/src/react/ai-chat/hooks/useAIChatEditor.slow.tsxpackages/ai/src/react/ai-chat/utils/submitAIChat.slow.ts等;
  • core / 其他包packages/core/src/react/components/Plate.slow.tsxpackages/yjs/src/lib/__tests__/collaboration/index.slow.tspackages/table/src/lib/merge/tableMergeBehavior.slow.tsx等。

这些文件不因所属目录而被特殊对待,慢车道身份完全来自文件名后缀——这正是「命名即分类」的直观体现。

相关 npm 脚本一览

双车道模型最终通过根目录 package.json 的脚本对外暴露,形成完整的使用入口:

脚本命令作用
testbun tooling/scripts/test-fast.mjs运行 fast 车道(*.spec.*
test:slowbun tooling/scripts/test-slow.mjs运行 slow 车道(*.slow.*),唯一的慢车道入口
test:allpnpm test && pnpm test:slow两条车道全量运行
test:slowestbun tooling/scripts/test-slowest.mjs执行快车道性能门槛检查(失败模式)
test:profilebun tooling/scripts/test-slowest.mjs --profile非失败的性能画像,输出慢速用例
test:deferredbun tooling/scripts/test-deferred.mjs运行__deferred__目录下的延期测试
test:watchbun tooling/scripts/test-fast.mjs --watchfast 车道监听模式,开发期使用
checkpnpm lint && pnpm typecheck && pnpm test:all && pnpm test:slowest全量门禁,含性能门槛强制检查

日常开发反馈循环使用pnpm test(快车道)即可;提交前跑pnpm check会同时触发两条车道与test:slowest性能门槛,确保「快车道不偷偷变慢」这一纪律在 CI 与本地都得到强制执行。

清理的验收与启示

对照计划文档的五步工作项,当前仓库的最终状态是:

  1. test-suites.mjs中已不存在基于路径的慢速分桶,慢车道模式仅由*.slow.{ts,tsx}后缀驱动;
  2. test-fast.mjs不再内置按路径排除的逻辑;
  3. 原被分桶的 docx、docx-io、package-integration 及 reactHeavy 测试均已重命名为.slow.ts[x],全仓库 77 个慢速测试文件遵循同一命名约定;
  4. test:slow是慢车道唯一运行入口;
  5. 「slow = 文件名后缀」的语义已固化进配置、脚本与性能门槛机制中。

这套「双车道 + 命名即分类 + 性能门槛」的组合,为大型编辑器 monorepo 提供了可复制的测试分层范式:命名约定让测试归属一目了然,性能门槛用数据强制维护车道纪律,而统一配置文件(tooling/config/test-suites.mjs)则让所有运行器共享唯一的分类事实来源。对于正在构建多包仓库测试体系的团队,这是一份可以直接借鉴的工程实践。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询