Plate Yjs 协作测试收割(Harvest)指南:许可证门控、可移植行为分类与适配层语料映射
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
导读
本文围绕 Plate 仓库中的yjs-collaboration-harvest计划(docs/plans/2026-05-13-yjs-collaboration-harvest.md)展开,系统讲解如何以许可证门控(license-gated)方式从四个上游仓库(slate-yjs、lexical-yjs、y-prosemirror、yjs)收割 Yjs 协作相关测试:建立可运行性清单、按可移植性分类、映射到 Slate v2 与 Plate 的归属方,并沉淀为只读证据产物。读完本文,你将掌握这套"测试收割—行为分类—归属映射"的方法论、各上游测试语料的全景盘点(约 54 个 slate-yjs 协作 fixture 与数百个 CRDT 层用例),以及它在 Plate 仓库内落地为packages/yjs慢速协作测试套件(含内存多端连接器CollaborationConnector)的具体源码实现与验证命令。
一、为什么需要"测试收割":协作行为证据的复用地基
富文本编辑器接入 Yjs 时,最大的工程风险不在于 CRDT 本身,而在于编辑器操作语义与共享文档模型之间的适配层:insertText、splitNode、mergeNode、moveNode、setNode、addMark、removeMark等 Slate 操作,需要被可靠地翻译成 Yjs 共享类型(Y.XmlText/Y.XmlElement)上的增量更新,并在远端以Y.Doc镜像的形式收敛。
该计划的核心判断是:这些行为已经以测试的形式存在于上游生态中,例如 slate-yjs 的协作 fixture、y-prosemirror 的 delta/position 不变量、yjs 自身的 CRDT 用例。与其从零设计测试,不如对上游测试做一次"收割"(harvest):
- 先做许可证门控:确认每个目标仓库的许可证允许复用其测试思路与语料;
- 再建穷举清点:把每个仓库中所有满足测试文件模式的路径找出来;
- 逐行分类:判断每条用例是"可移植行为"、"混合行为"、纯 harness、还是与编辑器行为无关的 CRDT 内部机制;
- 最后映射归属:把可移植行为指派给 Slate v2 与 Plate 的对应维护方。
该计划本身只留下报告类产物(inventory / test-index / completion 状态),不修改任何上游仓库,因此可安全、可追溯地作为后续slate-yjs包与 Plate Yjs 插件的测试语料来源。
二、收割目标与授权评估
计划声明的目标(Goal)原文如下:
Run a license-gated harvest of Yjs collaboration tests from
../slate-yjs,../lexical/packages/lexical-yjs,../y-prosemirror, and../yjs; classify portable behavior, map it to Slate v2 and Plate owners, and leave report-only artifacts.
四个目标仓库及其收割结论:
| 目标仓库 | 许可证结论 | 收割要点 |
|---|---|---|
slate-yjs | MIT/宽松 | 协作操作 fixture 通过适配器重放,并与远端Y.Doc镜像比对,是适配层行为的最直接语料 |
lexical/packages/lexical-yjs | MIT/宽松 | 在收割清点模式下没有任何可运行测试文件,仅能做源码级概念扫描 |
y-prosemirror | MIT/宽松 | delta / position / suggestion / undo 等大量"映射与变换不变量"测试,跨编辑器可移植性高 |
yjs | MIT/宽松 | CRDT 底层语料:相对位置、快照、更新合并、undo-redo、共享类型行为 |
许可证结论基于各仓库本地许可证证据得出,而非外部声明。这也是"许可证门控"的含义:先确认可复用的法律前提,再进入清点环节。
三、八阶段执行流程
该计划以完成清单(checklist)形式记录了八个阶段,全部处于完成状态([x]):
- Skill 分析与目标设定(owner skill 为
.agents/skills/editor-test-harvester/SKILL.md); - 解析目标仓库与许可证模式;
- 构建穷举测试清点(inventory);
- 抽取 fixture / 测试名称;
- 逐行分类(classify every row);
- 将行为映射到 Slate v2 与 Plate 归属方;
- 撰写报告、清点、测试索引与完成状态;
- 校验报告章节与完成检查。
这八个阶段构成了一条可复用的收割流水线:任何新编辑器适配层(例如未来接入其他 CRDT)都可以复用同样的"清点 → 抽取 → 分类 → 映射 → 沉淀"路径。
四、清点命令与产物结构
4.1 清点命令(Inventory Commands)
计划采用统一的rg管道来发现测试文件,模式同时覆盖目录命名与文件命名两类约定:
rg --files <repo> | \ rg '(^|/)(__tests__|test|tests|spec|e2e|integration|playwright|cypress|wdio|fixtures)(/|$)|\.(test|spec)\.[cm]?[jt]sx?$' | \ rg -v '(^|/)(dist|build|coverage|node_modules|vendor|fixtures/generated|__snapshots__)(/|$)' | \ sort该模式的关键点:
- 包含规则:目录名命中
__tests__、test、tests、spec、e2e、integration、playwright、cypress、wdio、fixtures,或文件名命中.test.*/.spec.*(含.cjs、.mjs、.js、.jsx、.ts、.tsx变体); - 排除规则:
dist、build、coverage、node_modules、vendor、fixtures/generated、__snapshots__等生成物与依赖目录; - 同一命令分别对四个仓库执行,保证口径一致、可复跑。
4.2 产物结构
收割完成后沉淀为三类证据产物(status: done,license_mode: permissive):
- Inventory(docs/editor-test-harvester/yjs-collaboration/inventory.md):逐文件的清点总表,包含可运行性、类别、判定理由、测试名抽取方式;
- Test Index(docs/editor-test-harvester/yjs-collaboration/test-index.md):逐条测试的精确索引(文件路径 + 行号 + 测试/导出名);
- 完成状态:以 completion ledger 形式记录(见后文"验证与完成检查"一节)。
需要说明:计划头部记录了报告路径docs/editor-test-harvester/yjs-collaboration/report.md,而在当前仓库快照中可直接验证的产物为上述 inventory 与 test-index 两个文件。
五、分类体系:可移植性判定
清点表对每一行都给出类别(Category),这是整份收割计划最核心的分析维度:
| 类别 | 含义 | 判定示例(取自 inventory) |
|---|---|---|
portable | 可移植行为,与编辑器适配层直接相关 | slate-yjs 协作 fixture:"Slate operation fixture is replayed through slate-yjs and checked against a remote Y.Doc mirror." |
portable-mixed | 可移植不变量与宿主编辑器策略(view/plugin/suggestion 等)混杂 | y-prosemirror 的suggestions.test.js:"Useful collaboration invariant mixed with ProseMirror view/plugin/suggestion policy." |
harness | 仅测试支撑代码,无独立行为断言 | withTestingElements.ts、cohort.js、testHelper.js等 |
skip | 跳过:与 Slate/Plate 编辑器行为目标无关 | yjs 的IdMap.tests.js:"Yjs internal storage/encoding compatibility... not a Slate or Plate editor-behavior target." |
manual | 需人工介入执行 | y-prosemirror 的tr.test.js、y-prosemirror.test.js(标记为 manual 但仍归类 portable-mixed) |
这个五分类的价值在于:收割方可以只把portable行纳入适配层测试设计,把portable-mixed拆出通用不变量、丢弃宿主策略部分,把skip行留给 CRDT 底层信心而非编辑器行为覆盖。
5.1 空目标:lexical-yjs
清点表专门记录了"空目标"(Empty Target Notes):
../lexical/packages/lexical-yjshas no test files under the required inventory pattern. Its source was scanned for Yjs collaboration concepts, but there is no runnable upstream test to harvest from that package path.
即 lexical-yjs 包在指定清点模式下没有任何可运行测试,只有源码级概念扫描记录——这是一个重要的"负结果":不能因为某仓库有 Yjs 集成包就默认它有可收割的测试语料。
六、上游测试语料全景盘点
6.1 slate-yjs:适配层 fixture 主力(54 个 fixture + 1 个适配器套件)
packages/core/test/collaboration/下按 Slate 操作类型组织的 fixture 是适配层行为的最直接证据,测试索引(test-index.md)逐一给出了文件与行号:
| 操作族 | fixture 数量 | 代表性用例 |
|---|---|---|
addMark | 5 | acrossMarks、acrossMarksSame、atBeginningOfDocument、atEndOfDocument、withOtherMarks |
insertNode | 3 | atBeginningOfDocument、atEndOfDocument、inTheMiddle |
insertText | 11 | insideMarks、inTheMiddleOfNestedBlock、withEmptyString、withEntities、withUnicode、withMarks等 |
mergeNode | 5 | afterADeleteBackward、inSameParent、onMixedNestedNodes、onMixedTypeNodes、withUnicode |
moveNode | 8 | downward/upward×whenBlockBecomesNested / BecomesNonNested / StaysNested / StaysNonNested |
removeMark | 3 | inTheMiddleOfText、withAddMark、withOtherMarks |
removeNode | 4 | nestedBlock、wrapperBlock、文档首/尾 |
removeText | 3 | 文档首/尾、withUnicode |
setNode | 6 | onDataChange、onDataChangeOnInline、onResetBlock、withAChangeOfType等 |
splitNode | 6 | onNonDefaultBlock、withMultipleSubNodes、withUnicode等 |
合计54 个 fixture,外加一个index.test.ts适配器套件(test-index.md 中记录其第 63 行为adaptersuite,负责加载全部协作 fixture 并校验本地/远端收敛)。辅助文件withTestingElements.ts(测试元素与共享根接线)与slate.d.ts(环境类型)被标记为 harness / skip。
从命名可以观察出覆盖设计的意图:Unicode 贯穿各操作族(withUnicode反复出现),嵌套结构(nested block、mixed nested nodes)与格式标记组合(withOtherMarks、insideMarks)是重点边界。
6.2 y-prosemirror:映射与变换不变量(116 个导出)
该仓库的测试大多以test*.js形式存在,由 lib0 runner(index.js/index.node.js)加载。按测试索引统计:
| 文件 | 可移植类别 | 导出数 | 覆盖主题 |
|---|---|---|---|
positions.test.js | portable | 22 | 单/多段落、硬换行、嵌套 blockquote、列表、代码块、深嵌套、storeMapping往返、远端变更后的书签映射 |
undo.test.js | portable-mixed | 33 | 基本撤销重做、历史分组、光标恢复、跨多撤销组、AddToHistory策略、视图销毁重建、远端变更不可撤销等 |
y-prosemirror.test.js | portable-mixed | 23 | 插件完整性、重叠标记、文档/XML 片段变换、change origin、空段落、重复插入、版本化、GC、RepeatGenerate压力序列 |
suggestions.test.js | portable-mixed | 21 | 建议模式下的标记同步、删除/回车/退格加入、双视图发散、cohort 重放收敛 |
delta.test.js | portable | 12 | 跨部分节点的删除区间、格式化、包裹、复杂 step 序列、带内容的 blockquote |
positions相关 harness | harness | — | cohort.js、complexSchema.js为共享仿真/schema 辅助 |
suggestion-simulation.test.js | portable-mixed | 4 | 仿真设置收敛、单建议编辑收敛、重复生成建议编辑、长跑 fuzz |
tr.test.js | manual | 1 | testReplaceStepToDelta |
合计约116 个导出。其中positions与delta两个文件被直接归类为portable,是"编辑器协作位置与 delta 映射不变量"的典范——这类不变量与宿主编辑器无关,可直接借鉴到 Slate 适配层。
6.3 yjs:CRDT 底层语料(237 个导出)
yjs 自身的测试由 lib0 runner(tests/index.js)加载,覆盖共享类型与协议机制:
| 文件 | 类别 | 导出数 | 与适配层的关系 |
|---|---|---|---|
y-text.tests.js | portable | 47 | delta 语义、格式化保留、Unicode/代理对拆分、embed、快照、attribution、大规模分片文档、RepeatGenerate*随机压力 |
y-array.tests.js | portable-mixed | 41 | 并发插入/删除冲突、晚同步、事件目标、GC、随机压力(最高 30000 次迭代) |
y-map.tests.js | portable-mixed | 40 | 嵌套事件、并发 set、属性冲突、attribution(最高 100000 次迭代) |
undo-redo.tests.js | portable | 25 | UndoManager作用域、删除过滤器、嵌套撤销问题、连续重做 bug、ignoreRemoteMapChanges |
snapshot.tests.js | portable | 12 | 快照恢复、删除项恢复、依赖变更、containsUpdate |
doc.tests.js | portable | 11 | 子文档(subdoc)加载/同步/undo、客户端 ID 冲突 |
relativePositions.tests.js | portable | 9 | 相对位置案例 1–7、与 undo 的结合、关联差异 |
updates.tests.js | portable | 8 | 更新合并、键编码、待处理更新合并、混淆、文档交集 |
attribution.tests.js | portable-mixed | 7 | 相对位置、attributed 事件、插入到带归属内容中 |
IdMap/IdSet.tests.js | skip | 7+7 | Yjs 内部存储/编码兼容性 |
compatibility.tests.js | skip | 3 | V1 编码解码兼容 |
encoding.tests.js | skip | 3 | 结构引用、state vector 差分 |
y-xml.tests.js | portable-mixed | 12 | XML 元素/属性、fragment attribution |
delta.tests.js | portable | 5 | delta 基础、schema、attribution |
合计约237 个导出。其中skip行(IdMap/IdSet/compatibility/encoding)被明确判定为"CRDT 底层信心"而非编辑器行为目标,这正是分类体系避免把无关语料误入适配层测试的有效例证。
七、关键发现:收割结论的三条主线
计划的关键发现(Key Findings)可归纳为三条,分别对应法律前提、负结果与增量机会:
- 许可证干净:四个目标仓库均为 MIT/宽松(基于本地许可证证据);
- lexical-yjs 无料可收:没有满足清点模式的可运行测试文件;
- 两侧现状差距:
- Slate v2 侧已有较强的协作底层覆盖,证据为
.tmp/slate-v2/packages/slate/test/collab-history-runtime-contract.ts(协作历史运行时契约测试); - Plate 侧已有有用的 Yjs 慢速 fixture,但仍有适配转换缺口:计划原文指出可受益于一个紧凑的适配器转换包(adapter-conversion pack),覆盖unicode、marks、nested moves、split/merge/set-node、cursor projection五类行为。
- Slate v2 侧已有较强的协作底层覆盖,证据为
这五类缺口与上文 slate-yjs fixture 表中的高频关键词(withUnicode、withMarks、嵌套 move、split/merge/set-node)完全对应,说明收割语料与缺口分析形成了闭环。
八、Plate 侧落地:packages/yjs的协作测试源码剖析
收割计划在 Plate 仓库内的落地证据位于 packages/yjs/src/lib/tests/collaboration/,由三个文件构成:fixtures.ts(10 个协作场景)、harness.ts(内存多端连接器与工具函数)、index.slow.ts(慢速测试入口)。
8.1 测试入口与运行方式
index.slow.ts把fixtures.ts导出的collaborationFixtures逐一注册为用例:
import { collaborationFixtures } from './fixtures'; describe('yjs collaboration', () => { afterEach(() => { mock.restore(); }); for (const fixture of collaborationFixtures) { it(fixture.name.replaceAll('_', ' '), fixture.run); } });文件名后缀.slow.ts与计划中"Plate already has useful Yjs slow fixtures"的表述相互印证:这些用例涉及多端连接、异步收敛与 setTimeout 打桩,属于慢速、确定性要求较高的协作测试。
8.2 核心装置:内存多端CollaborationConnector
harness.ts 用纯内存实现了一个微型多端协作网络,避免引入真实 WebSocket/WebRTC 依赖:
CollaborationConnector:维护peers映射与消息队列queue;connect时为新 peer 与已连接 peer 互发全量状态(Y.encodeStateAsUpdate);enqueue把本地更新广播给其他已连接 peer;flushAll({ order: 'fifo' | 'reverse' })批量投递更新(Y.applyUpdate(peer.document, update, REMOTE_ORIGIN)),reverse模式用于模拟乱序到达;TestCollaborationProvider:实现UnifiedProvider契约的测试端,挂在Y.Doc的update事件上采集本地更新并交给连接器;通过registerProviderType(PROVIDER_TYPE, TestCollaborationProvider)注册为动态 provider 类型;REMOTE_ORIGIN符号:区分本地/远端更新来源,避免远端更新被再次广播(回声抑制),这是收敛正确性的关键细节;- 工具函数:
createCollaborationEditor(用BaseYjsPlugin.configure构造编辑端)、createMixedProviderEditor(真实 provider + passive mock provider 混用)、getDocChildren(用@slate-yjs/core的yTextToSlateElement把Y.XmlText反解为 Slate 节点)、initEditor、replaceSharedContent/appendSharedContent(通过slateNodesToInsertDelta把 Slate 值写入共享类型)、settle(双微任务让异步收敛稳定)。
8.3 十类协作场景:fixtures 一览
fixtures.ts 定义的 10 个场景覆盖了"加入房间—并发—断线重连—乱序—超时—混合 provider"的全生命周期:
| fixture | 验证的行为不变量 |
|---|---|
seed_once_from_empty_doc | 空文档只播种一次,第二个 peer 收敛到同一内容 |
server_content_wins_over_local_value | 已有服务端内容时,本地 draft 不得覆盖远端(服务端优先) |
string_value_deserializes_once | 字符串初始值只反序列化一次(spy 断言deserialize调用次数),远端不再重复反序列化 |
async_value_waits_then_converges | 异步初始值:在 Promise resolve 前文档保持空,resolve 后收敛 |
custom_shared_type_nested_doc | 嵌套父文档中的Y.XmlText(parentDoc.getMap('editors').get('main'))作为共享类型,且根content保持为空 |
reconnect_eventually_converges | 断线期间远端更新,重连后两端收敛到更新值 |
concurrent_local_edits_while_disconnected_eventually_converge | 两端同时断线并各自追加内容,重连后两端一致且内容完整(hello world + peer one + peer two) |
out_of_order_updates_eventually_converge | 以reverse顺序投递更新,最终顺序依然正确 |
timeout_then_late_sync_does_not_reseed | runWithImmediateTimeout模拟超时后迟到的同步不得再次播种覆盖 |
mixed_provider_inputs | 同一Y.Doc挂载多个 provider(真实 + passive),连接行为正确 |
这些场景与收割计划"适配器转换包"的五类缺口(unicode、marks、nested moves、split/merge/set-node、cursor projection)形成互补:Plan 侧缺口指向操作级适配,而 Plate 现有 fixture 已经覆盖同步生命周期级不变量(播种、服务端优先、断线并发、乱序、超时重连)。
8.4 插件侧的配套实现
协作测试所依赖的运行时能力分布在 packages/yjs/src/lib:
- BaseYjsPlugin.ts:核心绑定逻辑,持有
Y.Doc、Awareness、sharedType与 providers; - withPlateYjs.ts:编辑器级集成(含
withTCursors光标投影、withTYHistory历史、withTYjs共享类型绑定); - providers/registry.ts 与三个 provider 包装(hocuspocus-provider.ts、indexeddb-provider.ts、webrtc-provider.ts);
- createMockProvider.ts 与 mockFn.ts:混合 provider 测试的被动端。
React 侧入口 YjsPlugin.tsx 提供YjsPlugin.configure({ options: { providers, ydoc, sharedType } })的声明式配置;插件的安装与 provider 参数说明(indexeddb/hocuspocus/webrtc及自定义 provider 注册)见 packages/yjs/README.md。
九、验证与完成检查
计划记录了完成验证步骤,其中仓库内可复跑的核心命令如下:
rg -n "License Gate|Confidence Score|Pass-State Ledger|Matrix|Skips|Next Slice|Full Inventory Appendix" docs/editor-test-harvester/yjs-collaboration/report.md test -f docs/editor-test-harvester/yjs-collaboration/inventory.md test -f docs/editor-test-harvester/yjs-collaboration/test-index.md bun run completion-check -- --id 019e1c53-3e25-78c0-9083-355925be3817- 第一条用
rg验证报告包含全部关键章节(许可证门控、置信度评分、通过状态台账、矩阵、跳过、下一切片、完整清点附录); - 第二、三条验证收割产物文件存在(当前仓库中
inventory.md与test-index.md均可直接确认); - 第四条通过
bun run completion-check以稳定 ID 校验完成状态。
这套验证方式的特点是可机械复跑:任何后续收割都能用同样的命令证明"产物齐备、章节完整"。
十、从收割语料到slate-yjs包的演进脉络
计划头部包含三条同步记录(Sync note),说明了该文档在时间线上的定位——它始终是证据清单(evidence inventory),而非当前 API 决策:
- 2026-05-18:包级规划迁至 docs/plans/2026-05-18-slate-yjs-package-readiness-ralplan.md;当时
packages/slate-yjs尚无源码,剩余执行工作为包脚手架、完整仿真示例、包测试与 Playwright 选区覆盖; - 2026-05-24:继续以稳定收割产物作为测试语料,但不得据此推断当前 API 或包存在性;
- 2026-05-28:
../slate-v2中已出现@slate/yjs包源码,当前架构与操作矩阵工作迁至 docs/plans/2026-05-28-slate-yjs-current-architecture-operation-matrix.md。
后续相关计划还包括 2026-05-25-slate-yjs-structural-operation-coverage-ralplan.md 与 2026-05-29-slate-yjs-from-scratch-operation-matrix.md,并有 本地 yjs 别名避免重复安装的解决方案记录。这条脉络展示了测试收割在工程中的正确用法:语料沉淀一次、长期复用,而 API 与架构决策跟随最新计划演进。
十一、方法论复用:给其他适配层(或未来 CRDT)的收割清单
把本文内容浓缩为可迁移的步骤,任何新的编辑器协作适配层都可以按此执行:
- 门控:先确认上游许可证(本地证据),再谈语料复用;
- 清点:用统一的
rg管道对每个上游仓库跑同一套包含/排除规则; - 分类:逐文件打
portable / portable-mixed / harness / skip / manual标签,并为每个标签写下判定理由; - 抽名:把每条可运行测试的文件路径、行号、导出名沉淀为 test-index;
- 映射:把可移植行为指派给编辑器核心、适配层与插件层的具体归属方;
- 沉淀:只写报告类产物,并记录完成状态(status / license_mode / completion);
- 闭环:把"缺口清单"(如 unicode、marks、nested moves、split/merge/set-node、cursor projection)回填到自身测试设计,用内存多端连接器实现可确定性验证的慢速协作套件。
结语
Yjs 协作的正确性最终由"操作语义翻译 + 同步生命周期收敛"共同保证,二者缺一不可。本文所讲解的收割计划给出了前半部分(操作级适配)的系统化语料来源——54 个 slate-yjs 协作 fixture、116 个 y-prosemirror 映射不变量、237 个 yjs CRDT 用例,并明确了哪些可移植、哪些应跳过;而 Plate 仓库内的packages/yjs慢速协作套件与内存CollaborationConnector则示范了后半部分(生命周期收敛)的可确定性验证方式。两者结合,构成了一套从上游证据到本地落地、可复跑、可追溯的协作测试方法论。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考