Ghost Koenig Lexical 编辑器开发与测试工作流指南:从 AGENTS 规范到 Playwright/Vitest 工程实践
Koenig Lexical(@tryghost/koenig-lexical)是 Ghost 基于 Meta Lexical 框架实现的富文本文章编辑器。本文以仓库内 koenig/koenig-lexical/AGENTS.md 为核心脉络,系统梳理该包面向开发者与 AI Agent 的统一工作流:pnpm 命令约定、单元测试与验收测试的组织方式、"AI 友好型测试"的默认行为,以及如何把编辑器以独立模式或集成模式跑起来并完成提交流程。
1. Koenig Lexical 在 Ghost 中的位置
Koenig 是 Ghost 的卡片化编辑器体系,而 koenig-lexical 是其中基于 Lexical 的最新实现(package.json 中描述为 "Ghost's Lexical-based rich text post editor",版本号示例为 1.9.4)。它与 koenig 目录下的多个工作区包协同工作:
- koenig/kg-default-nodes:默认卡片节点(audio、video、callout、toggle 等)的 Lexical 实现;
- koenig/kg-default-transforms:默认文本变换规则(如 emdash/en-dash、链接、标题等);
- koenig/kg-converters、koenig/kg-html-to-lexical、koenig/kg-lexical-html-renderer:文档格式与 HTML 相互转换链路。
在修改本包前,务必先阅读 koenig/koenig-lexical/README.md,尤其是其中的 Development、Testing 与 Editor integration 三个章节——这是 AGENTS.md 明文要求的首要动作。另外同目录下的 koenig/koenig-lexical/CLAUDE.md 与 AGENTS.md 内容一致,供不同 Agent 工具链读取同一套规则。
2. 必守工作流:pnpm 与测试分层
AGENTS.md 定义了本包开发的"必要工作流"(Required workflow),核心约束有三条。
2.1 统一使用 pnpm
整个 Ghost 仓库是 pnpm workspace monorepo(见根目录 pnpm-workspace.yaml 与 package.json),因此任何安装、构建、测试命令都必须以 pnpm 执行,不要混用 npm/yarn。
2.2 理解两层测试体系的分工
AGENTS.md 特别强调测试分层,防止混淆:
- 本包
package.json中的 Playwright 测试是包级验收测试(package-level acceptance tests),验证编辑器在真实浏览器中的行为,测试文件位于koenig/koenig-lexical/test/e2e/; - Ghost 真正的浏览器 端到端(E2E)套件位于仓库顶层的
e2e/workspace(即 e2e/ 目录),面向完整产品流程。
改动编辑器后,应根据影响范围选择在包级运行验收测试,或在仓库级跑 E2E,二者不可互相替代。
2.3 先聚焦后全量,提交前双门禁
推荐的迭代节奏是:
- 开发期间用
pnpm test:unit:watch进行聚焦的单元测试开发(watch 模式,改动即重跑); - 需要精简验收测试输出时用
pnpm test:acceptance:quiet(只显示失败用例); - 反复运行相关的聚焦测试直到通过;
- 提交前最终执行
pnpm test与pnpm lint全部通过。
3. AI 友好型测试:默认行为的工程细节
AGENTS.md 中最具特色的部分是 "AI-Friendly Testing"——测试运行器被专门配置为对 AI Agent 友好,确保机器执行时的确定性。其四项默认行为都可以在配置中找到对应实现。
3.1 默认无头模式 + list reporter
AGENTS.md 规定默认行为是 Headless(无浏览器 UI、不打开网页),配合 list reporter 输出清晰的 pass/fail 信息。对应实现位于 koenig/koenig-lexical/playwright.config.ts:
reporter: process.env.CI ? [['github'], ['html']] :
process.env.PLAYWRIGHT_HTML_REPORT ? [['html'], ['list']] :
[['list']],
而 headless 则读取 PLAYWRIGHT_HEADED 环境变量:
headless: process.env.PLAYWRIGHT_HEADED ? false : true
也就是说:不加任何环境变量时,浏览器静默运行、只输出 list 报告;这正是"AI 可直接解读输出、无挂起进程、不弹浏览器窗口"的原因。
3.2 静默模式与干净退出
pnpm test:acceptance:quiet实际执行的是pnpm test:acceptance --reporter=line(见 package.json 的 scripts),把输出压缩到只显示失败;- 测试完成即退出,不会遗留浏览器进程或报告服务器——因为只有显式指定
PLAYWRIGHT_HTML_REPORT=true时才启用 html reporter,--ui/ headed 模式也只是在需要交互式调试时才开启。
3.3 何时才允许交互式调试
AGENTS.md 明确列出三个"仅在交互式调试有用时才使用"的命令,且验证完毕后不得残留浏览器或报告服务器:
pnpm test:acceptance:headed:带可见浏览器的验收测试;pnpm test:acceptance --ui:Playwright UI 模式(watch 调试);pnpm test:slowmo:带 100ms 步进延迟的 headed 测试。
以 slowmo 为例,它的实现会同时放大超时阈值以避免误报超时:
"test:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance:headed"
在 playwright 配置中,PLAYWRIGHT_SLOWMO 通过 launchOptions.slowMo 注入浏览器(playwright.config.ts),并且在 CI 环境默认启用 2 次重试与 forbidOnly(防止 test.only 被误提交)。
4. 从 AGENTS 指令还原出的完整命令矩阵
AGENTS.md 只点出了最常用的 4 个命令,而完整矩阵定义在 koenig/koenig-lexical/package.json 与 README 中,建议组合使用:
| 命令 | 作用 | 适用时机 |
|---|---|---|
pnpm test |
运行全部测试(unit + acceptance)后退出 | 提交前全量回归 |
pnpm test:unit |
typecheck + 运行单元测试 | 快速校验逻辑 |
pnpm test:unit:watch |
watch 模式,文件变更自动重跑单元测试 | 聚焦单元测试开发 |
pnpm test:unit:watch -t "buildCardMenu" |
按 describe/it 关键字过滤测试 |
只跑某组用例 |
pnpm test:acceptance |
无头运行 Playwright 验收测试 | 默认验收回归 |
pnpm test:acceptance:quiet |
--reporter=line,仅输出失败 |
精简输出(AI 场景推荐) |
pnpm test:acceptance:headed |
带浏览器运行 | 交互式调试 |
pnpm test:acceptance:report |
生成 HTML 报告 | CI 失败分析 |
pnpm test:slowmo |
100ms 慢动作 headed 模式 | 观察逐步交互 |
pnpm lint |
依次运行 lint:css、lint:js |
提交前门禁 |
pnpm typecheck |
tsc --noEmit -p tsconfig.typecheck.json |
类型检查 |
4.1 运行前置条件:pretest 钩子
在开始测试之前,包会先构建一份 demo 应用用于测试,这由 pretest 钩子完成:
"pretest": "VITE_TEST=true pnpm build --config vite.config.demo.ts && pnpm build"
因此首次跑 pnpm test 时会先产出 demo 构建产物再启动测试,请勿在构建阶段手动中断。
4.2 ESM 环境下的验收测试约束
README 特别提醒:由于 package.json 声明了 "type": "module",验收测试运行在 Node ESM 模式下,会遇到三类限制——不能使用 require/__filename 等 CommonJS 全局量、导入必须带完整文件扩展名、不能依赖 require.extensions。包内通过 Playwright 脚本注入两个参数来缓解:
"test:acceptance": "NODE_ENV=test PLAYWRIGHT_FORCE_ASYNC_LOADER=1 NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test"
其中 PLAYWRIGHT_FORCE_ASYNC_LOADER=1 的来龙去脉在 playwright.config.ts 的注释中有详细解释:Node 22.15+ 的同步模块钩子无法处理"对 ESM-only 包的 CJS require()"(jsdom 29 依赖链会报 "request for X is not in cache"),而异步 loader 路径可正常处理;Node 24.x 已修复,待 workspace 升级后可移除该标志。实践中若某些常量恰好放在 .jsx 文件里导致 ESM 导入报错,处理方式是将其移动到 .js 文件。
5. 单元测试与验收测试的工程组织
5.1 测试目录约定
测试遵循统一布局(见 koenig/koenig-lexical/test):
test/unit/:Vitest 单元测试,如CardMenu.test.tsx、buildCardMenu.test.ts、hooks/useVisibilityToggle.test.ts、utils/generateEditorState.test.ts;test/e2e/:Playwright 验收测试,又按行为分组组织:test/e2e/cards/:每个卡片一个文件(audio、bookmark、button、callout、toggle、signup、product、image、html 等);test/e2e/editors/:basic-editor、email-editor、minimal-editor;test/e2e/plugins/:DragDrop、EmojiPicker、WordCount 等插件行为;test/e2e/text-transforms/:文本变换规则;- 以及
card-behaviour.test.ts、paste-behaviour.test.ts、floating-toolbar.test.ts、slash-menu.test.ts、list-behaviour.test.ts等总览级用例;
test/utils/:被两层测试共享的辅助工具,入口为e2e.ts;test/test-setup.ts:Vitest 的全局 setup(通过 vite 配置中的setupFiles注入)。
两个浏览器项目(chromium + firefox)的划分也很清晰:默认跑 chromium,凡文件名带 firefox 标记的用例(如 video-card.firefox.test.ts、DragDropPastePlugin.firefox.test.ts)只匹配 Firefox 项目(见 playwright.config.ts)。如果你在给这些用例补覆盖,请留意 testIgnore/testMatch 的正则命名约定。
5.2 共享测试辅助:从初始化到断言
包级验收测试高度依赖 koenig/koenig-lexical/test/utils/e2e.ts,理解这些辅助函数有助于编写风格一致的用例:
initialize({page}):导航到http://localhost:5174(端口来自 playwright.config 导出的E2E_PORT),固定 1000×1000 视口,等待.koenig-lexical渲染,然后把 Lexical 实例暴露到window.lexicalEditor上供后续读取;重复导航则通过 React Router 复用页面并重置编辑器状态;insertCard({cardName, nth}):通过键入/卡片名触发斜杠菜单并回车插入卡片,随后按data-kg-card断言卡片可见——这是所有卡片验收测试的通用入口;paste / pasteText / pasteHtml / pasteLexical / pasteFiles:以构造ClipboardEvent的方式模拟不同 MIME 类型的粘贴行为,覆盖text/plain、text/html、application/x-lexical-editor与文件粘贴(fixtures 目录里准备了损坏/正常的 mp3、mp4、图片以覆盖失败路径);assertHTML({selector, ignoreClasses, ignoreInlineStyles, ...}):对编辑器 innerHTML 做规整化(去掉 class、行内样式、SVG 内容、base64 数据、拖拽属性等)后再做快照比对,使断言对与业务无关的细节不敏感;assertRootChildren/getEditorStateJSON:直接比对 Lexical EditorState 的 JSON 结构;selectBackwards / selectForward / dragMouse / createDataTransfer:模拟键盘选区与鼠标拖拽交互。
例如一个典型的标题测试可写作:
await initialize({page});
await focusEditor(page);
await page.keyboard.type('# 标题');
await assertHTML(page, html`<h1>标题</h1>`);
5.3 单元测试的 jsdom 环境与关键字过滤
单元测试配置在 koenig/koenig-lexical/vite.config.ts 的 test 段:globals: true(供 jest-dom 扩展使用)、environment: 'jsdom'、setupFiles: './test/test-setup.ts'、默认 10s 超时,CI 下限制线程数避免资源竞争。由于预构建了 demo 版本用于单测,开发时可配合 -t 关键字快速圈定用例:
pnpm test:unit:watch -t "buildCardMenu"
6. 两种开发模式:Standalone 与 Integrated
虽然 AGENTS.md 没有展开运行细节,但其要求先读的 README 明确了编辑器的两种运行形态,这也是测试得以开展的前提。
6.1 Standalone(独立演示模式)
pnpm dev
该命令并行启动 Vite dev server、包构建 watch、preview 服务器,以及 kg-default-nodes/kg-default-transforms 的 watch 构建(见 package.json 中 dev 脚本)。编辑器演示页运行在 http://localhost:5173,由 index.html 渲染 demo/demo.tsx 中的全功能空编辑器(demo 应用源码在 koenig/koenig-lexical/demo 下,含 DemoApp、HtmlOutputDemo、RestrictedContentDemo 等演示视图)。
特殊卡片本地配置:
- Gif 卡片:需要在
koenig-lexical包根目录新建.env.local并提供 Klipy API key:VITE_KLIPY_API_KEY=xxx - Bookmark / Embed 卡片:这两类卡片会发起外部网络请求,而 demo 没有后端服务,需要在前端直接跨域抓取资源,因此要启用 CORS(README 建议使用浏览器扩展如 'Test CORS');否则会看到被浏览器拦截的请求错误。也可以直接使用测试数据、跳过
fetchEmbed.js的抓取逻辑来规避。
6.2 Integrated(集成到 Ghost Admin)
由于所有 Koenig 包都属于 Ghost monorepo workspace,无需手动 link,在仓库根目录运行:
pnpm dev:lexical
它启动常规 Ghost 开发环境,外加编辑器的 dev:integrated 目标:为编辑器(连同 kg-default-nodes / kg-default-transforms)启动 rebuild watcher,并在 4173 端口启动 preview 服务器;dev gateway 会把 Admin 的 EDITOR_URL 代理到这个 preview 服务。
随后访问 http://localhost:2368/ghost 打开任意文章,Admin 就会加载你本地构建的编辑器版本——改动会在几秒内(编辑器重建完成后)反映到 Ghost Admin 中。这是验证"编辑器真正集成进后台"的推荐路径。
7. 提交前检查单与规范维护
AGENTS.md 给出了收敛式的规范维护策略:
- 最终验证:在反复运行聚焦测试后,提交前必须通过
pnpm test与pnpm lint; - 不要重复记录规范:当包的共享命令或测试工作流发生变更时,更新对象是 koenig/koenig-lexical/README.md(其 Testing 章节维护了命令矩阵与环境变量
PLAYWRIGHT_HEADED、PLAYWRIGHT_HTML_REPORT、PLAYWRIGHT_SLOWMO的完整说明),而不要在 AGENTS.md 里复制一份导致双源漂移; - 发布流程:Koenig 系列包作为 Ghost monorepo workspace 的一部分统一版本化与发布,发布细节见 koenig/README.md 的 shipping 章节。
8. 小结:把这套工作流落到日常开发
从 AGENTS.md 出发可以提炼出 Koenig Lexical 开发的三条黄金法则:
- 命令走 pnpm,分层跑测试——单元测试
pnpm test:unit:watch快速迭代,验收测试用pnpm test:acceptance:quiet精准反馈,最终pnpm test && pnpm lint收口;浏览器级 E2E 交给仓库顶层e2e/workspace,勿与包级 Playwright 用例混淆。 - 默认行为面向确定性——无头 + list reporter + 干净退出让 AI Agent 也能稳定驱动;headed /
--ui/ slowmo 仅用于需要肉眼看交互的时刻,用完即关。 - 规范单一来源——共享命令与工作流变更写进 README,AGENTS.md 保持精简,避免两处维护产生分歧。
对 AI 驱动的代码生成场景而言,这套规范本身就是一份高质量的"机器可读需求文档":先读 README 建立背景,用 watch/quiet 模式获得最小反馈环,再以全量测试与 lint 作为提交门禁——这正是 Ghost 编辑器团队在 koenig/koenig-lexical/AGENTS.md 中希望每个协作者(无论人类还是 Agent)遵守的协作契约。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00