chore: 收口 Batch C 证据与设计口径
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# Batch C Worker A:Resource Open Resolver 最小收敛
|
||||
|
||||
项目根目录:`/mnt/Data1T/mnote`
|
||||
|
||||
你不是单独在代码库里工作,可能有其他 worker 并行执行。不要回滚、覆盖或格式化与你任务无关的文件;不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
Batch B 中 `design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md` 仍未完成。当前重复判断主要涉及:
|
||||
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `buildLocalOnlyOfficeOpenUrl`
|
||||
- `openEditorAttachmentEditTab`
|
||||
- `openEditorAttachmentNewWindow`
|
||||
- `openConvexAssetFromFileTree`
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- `resolveResourceOpen`
|
||||
|
||||
本次优先只改 `layout.rs` 和 `scripts/task463-onlyoffice-resolver-smoke.js`,不要大重构。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 审查正文附件和 FileTree asset 打开 office/resource 时的重复判断。
|
||||
2. 如果安全,抽出最小 helper,使正文附件“弹窗编辑”与 FileTree active tab 打开共用同一套 local Office URL 构造或打开目标判断。
|
||||
3. 保持现有行为:
|
||||
- 正文附件“弹窗编辑”进入主编辑器 resource tab。
|
||||
- 正文附件“新窗口编辑”进入浏览器新窗口。
|
||||
- md/text/code 继续走 tiptap/text/code 渲染路径,不改成 OnlyOffice 或新窗口默认打开。
|
||||
4. 更新 `5-26` checklist 的执行记录;通过后可勾选已完成项。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `scripts/task463-onlyoffice-resolver-smoke.js`
|
||||
- `design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md`
|
||||
- 可新增 `.codex/reasonix-tasks/results/batch-c-worker-a-resource-open-resolver.md`
|
||||
|
||||
禁止修改:
|
||||
|
||||
- OnlyOffice callback / 保存链路
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`,除非只改测试断言注释且非常必要
|
||||
- 其它 worker ownership 文件
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell -- --test-threads=1
|
||||
node --check scripts/task463-onlyoffice-resolver-smoke.js
|
||||
```
|
||||
|
||||
如果 `http://127.0.0.1:3000` 可用,再运行:
|
||||
|
||||
```bash
|
||||
node scripts/task463-onlyoffice-resolver-smoke.js
|
||||
```
|
||||
|
||||
## 输出要求
|
||||
|
||||
最终写入 `.codex/reasonix-tasks/results/batch-c-worker-a-resource-open-resolver.md`,包含:
|
||||
|
||||
- 修改文件列表。
|
||||
- 实际收敛了哪些重复判断。
|
||||
- 验证命令和结果。
|
||||
- 未完成项和风险。
|
||||
@@ -0,0 +1,53 @@
|
||||
# Batch C Worker B:GFM AST 尾项小包
|
||||
|
||||
项目根目录:`/mnt/Data1T/mnote`
|
||||
|
||||
你不是单独在代码库里工作,可能有其他 worker 并行执行。不要回滚、覆盖或格式化与你任务无关的文件;不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
`design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md` 仍有尾项:
|
||||
|
||||
- 空段落 / 空表格单元格 / 空引用块处理规则未固定。
|
||||
- legacy block -> Tiptap -> legacy marks 缺测试。
|
||||
- 保存失败前端状态保留缺测试。
|
||||
- 写侧手写 Markdown 函数未标记为过渡实现。
|
||||
- `web_shell.rs` legacy JS 适配分支仍无过渡说明。
|
||||
|
||||
本次只做低风险小包,不修复 frontmatter title 优先级。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 补一个或多个 Rust 单测,固定空表格单元格 / 空引用块等当前行为。
|
||||
2. 补 web_shell 相关测试或断言,证明 legacy->Tiptap inline marks 转换至少不会丢核心 marks。
|
||||
3. 给写侧手写 Markdown 回写函数和 web_shell legacy 适配分支加简短中文过渡注释。
|
||||
4. 更新 `3-13` 执行记录和 checklist 状态。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- `design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
- 可新增 `.codex/reasonix-tasks/results/batch-c-worker-b-gfm-tail.md`
|
||||
|
||||
禁止事项:
|
||||
|
||||
- 不修改 frontmatter title > H1 > 文件名行为。
|
||||
- 不删除 legacy 适配函数。
|
||||
- 不改 Page Aggregate 读取主链。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown -- --test-threads=1
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web web_shell -- --test-threads=1
|
||||
```
|
||||
|
||||
## 输出要求
|
||||
|
||||
最终写入 `.codex/reasonix-tasks/results/batch-c-worker-b-gfm-tail.md`,包含:
|
||||
|
||||
- 修改文件列表。
|
||||
- 关闭/追认了哪些 `3-13` 尾项。
|
||||
- 验证命令和结果。
|
||||
- 未完成项和风险。
|
||||
@@ -0,0 +1,64 @@
|
||||
# Batch C Worker C:Convex Export 产品化补 Smoke
|
||||
|
||||
项目根目录:`/mnt/Data1T/mnote`
|
||||
|
||||
你不是单独在代码库里工作,可能有其他 worker 并行执行。不要回滚、覆盖或格式化与你任务无关的文件;不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
`design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md` 的 Gap C1 已确认 export 脚本支持:
|
||||
|
||||
- `--dry-run`
|
||||
- `--manifest`
|
||||
- `--conflict-report`
|
||||
- `--rollback`
|
||||
|
||||
但仍缺独立 smoke 验证:
|
||||
|
||||
- dry run 不写文件
|
||||
- 同名冲突不覆盖
|
||||
- rollback 恢复导出前状态
|
||||
|
||||
## 目标
|
||||
|
||||
1. 新增或扩展 smoke,覆盖 dry-run / conflict / rollback 三个行为。
|
||||
2. 优先复用 `scripts/task444-convex-workspace-export-local-fixture-smoke.js` 和 `scripts/export-convex-workspace-to-local.js`。
|
||||
3. smoke 必须可离线运行,不依赖真实 Convex 服务。
|
||||
4. 更新 `1-6` Gap C1 与本轮 Batch C checklist。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `scripts/task444-convex-workspace-export-local-fixture-smoke.js`
|
||||
- 可新增 `scripts/task48*-convex-export-*.js`
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/process/1-10-batch-c-p1-resource-buffer-gfm-convex-checklist-v1.md`
|
||||
- 可新增 `.codex/reasonix-tasks/results/batch-c-worker-c-convex-export-smokes.md`
|
||||
|
||||
禁止事项:
|
||||
|
||||
- 不修改 Convex runtime / schema。
|
||||
- 不改变 local-first 架构口径。
|
||||
- 不让 smoke 写入仓库内非 tmp 目录。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
node --check scripts/task444-convex-workspace-export-local-fixture-smoke.js
|
||||
node scripts/task444-convex-workspace-export-local-fixture-smoke.js
|
||||
```
|
||||
|
||||
如果新增 smoke:
|
||||
|
||||
```bash
|
||||
node --check scripts/<new-smoke>.js
|
||||
node scripts/<new-smoke>.js
|
||||
```
|
||||
|
||||
## 输出要求
|
||||
|
||||
最终写入 `.codex/reasonix-tasks/results/batch-c-worker-c-convex-export-smokes.md`,包含:
|
||||
|
||||
- 修改文件列表。
|
||||
- dry-run / conflict / rollback 的验证方式。
|
||||
- 验证命令和结果。
|
||||
- 未完成项和风险。
|
||||
@@ -0,0 +1,31 @@
|
||||
# Batch C Worker A — Resource Open Resolver
|
||||
|
||||
> 执行时间:2026-05-21
|
||||
> 状态:`PLAN_ONLY_TIMEOUT`
|
||||
|
||||
## 结论
|
||||
|
||||
Worker A 没有生成 `final.md` / `result.json`,也没有落下可复核的业务代码 diff。本轮不采纳为完成项,不把 `5-26` checklist 标完成。
|
||||
|
||||
## Transcript 中可参考的计划
|
||||
|
||||
Worker A 仅提交了一个计划:
|
||||
|
||||
- 新增 `openLocalOfficeInActiveTab(detail, mode)` 辅助函数。
|
||||
- 让 `openConvexAssetFromFileTree` 的 local Office active-tab 分支和 `openEditorAttachmentEditTab` 复用该 helper。
|
||||
- `openEditorAttachmentNewWindow` 与 `openEditorAttachmentDetail` 暂不改,避免范围扩大。
|
||||
|
||||
## Codex 裁定
|
||||
|
||||
该计划方向可作为后续参考,但必须由 Codex 或新 worker 小步实现并复跑:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell -- --test-threads=1
|
||||
node --check scripts/task463-onlyoffice-resolver-smoke.js
|
||||
node scripts/task463-onlyoffice-resolver-smoke.js
|
||||
```
|
||||
|
||||
## 风险
|
||||
|
||||
- 原 worker 未产出 patch,不能作为完成证据。
|
||||
- `5-26` 继续保持 process 状态。
|
||||
@@ -0,0 +1,93 @@
|
||||
# Batch C Worker B:GFM AST 尾项小包
|
||||
|
||||
> 执行日期:2026-05-21
|
||||
> 执行人:Reasonix Code (deepseek-v4-flash)
|
||||
|
||||
---
|
||||
|
||||
## 修改文件列表
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `rust/crates/mnote-web/src/routes/local_markdown_parser.rs` | 编辑 | 新增 2 个解析测试 |
|
||||
| `rust/crates/mnote-web/src/routes/local_folder_source.rs` | 编辑 | 给 3 个写侧函数加过渡注释 |
|
||||
| `rust/crates/mnote-web/src/routes/web_shell.rs` | 编辑 | 测试断言扩展 + JS 适配层过渡注释 |
|
||||
| `design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md` | 编辑 | 更新 checklist 状态 |
|
||||
| `.codex/reasonix-tasks/results/batch-c-worker-b-gfm-tail.md` | 新建 | 本报告文件 |
|
||||
|
||||
---
|
||||
|
||||
## 关闭/追认的 3-13 尾项
|
||||
|
||||
### ✅ 11.2 空表格单元格 / 空引用块处理规则
|
||||
|
||||
- **追加测试** `markdown_empty_blockquote_parse`:验证 `>` / `> ` 输出 `type=quote, content=[]`
|
||||
- **追加测试** `markdown_table_empty_cells_parse`:验证空表头/数据单元格的 `paragraph.content` 为 `[]`
|
||||
- 行为固定为 comrak 默认输出,不再无规则
|
||||
|
||||
### ✅ 11.4 legacy→Tiptap→legacy marks 不丢 marks
|
||||
|
||||
- `document_shell_bootstrap_preserves_inline_mark_conversion` 原仅断言 `code` / `link`
|
||||
- **已扩展断言**:`bold`, `italic`, `underline`, `strike`, `code`, `link` 全部 6 种 core marks 的 JS 生成代码均存在于 HTML 中
|
||||
|
||||
### ✅ 11.8 web shell 单测:legacy↔Tiptap 不丢 marks
|
||||
|
||||
- 同上 — 测试名称不变但断言已补全
|
||||
|
||||
### ✅ 11.9 手写 parser 标记为过渡实现
|
||||
|
||||
- `inline_nodes_to_markdown`(`local_folder_source.rs:7004`):加中文过渡注释
|
||||
- `editor_blocks_to_markdown_for_file`(`local_folder_source.rs:6221`):加中文过渡注释
|
||||
- `editor_blocks_to_markdown_with_rewrite`(`local_folder_source.rs:6393`):加中文过渡注释
|
||||
|
||||
### 📌 11.9 web_shell.rs 临时适配分支
|
||||
|
||||
- **未删除**,但加了过渡注释 `// 过渡适配(TODO step-4)`
|
||||
- 完整标记了 `legacyStylesToTiptapMarks` / `legacyMarkArrayToTiptapMarks` / `mergeTiptapMarks` / `legacyInlineContentToTiptap` 四函数组的迁移计划
|
||||
|
||||
---
|
||||
|
||||
## 验证命令与结果
|
||||
|
||||
### 命令 1:`local_markdown` 测试组
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown -- --test-threads=1
|
||||
```
|
||||
|
||||
**结果:30 passed, 0 failed**
|
||||
|
||||
新测试确认通过:
|
||||
- `routes::local_markdown_parser::tests::markdown_empty_blockquote_parse`
|
||||
- `routes::local_markdown_parser::tests::markdown_table_empty_cells_parse`
|
||||
|
||||
### 命令 2:`web_shell` 测试组
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web web_shell -- --test-threads=1
|
||||
```
|
||||
|
||||
**结果:10 passed, 0 failed**
|
||||
|
||||
核心测试确认通过:
|
||||
- `routes::web_shell::tests::document_shell_bootstrap_preserves_inline_mark_conversion`
|
||||
|
||||
---
|
||||
|
||||
## 未完成项与风险
|
||||
|
||||
### 未完成项(保持 [ ] 状态)
|
||||
|
||||
| 项 | 说明 | 原因 |
|
||||
|----|------|------|
|
||||
| 11.2 标题优先级 frontmatter > H1 > 文件名 | 任务明确排除 | 本包不做 frontmatter title 优先级修复 |
|
||||
| 11.6 降级策略测试 | 仅部分完成 | 列对齐已追认、空单元格已补测试;但 `unsupported → paragraph` 等降级策略无专项测试 |
|
||||
| 11.7 保存失败状态保留 | 未触及 | 需要前端侧 error handler + 状态保留测试,属不同责任域 |
|
||||
| 11.9 web_shell.rs 临时适配分支移除 | 未删除 | 仅加过渡注释 — JS 侧函数仍需等 Step 4 才能移除 |
|
||||
| 11.9 process→done | 未移动 | 以上 [ ] 项全部关闭后执行 |
|
||||
|
||||
### 风险
|
||||
|
||||
1. **空单元格解析层 vs 回写层行为可能不一致**:解析层测试已验证空的 `paragraph.content = []`,但回写侧 `editor_blocks_to_markdown_with_rewrite` 对空单元格的处理未覆盖测试(它的流程是 `inline_nodes_to_markdown` → 空字符串 → `.trim().to_string()` → 空 → `replace('\n', " ")` → 保持空)。建议后续补 save 侧空单元格 round-trip 测试。
|
||||
2. **过渡注释不产生编译期警告**:当前使用普通 `//` 注释而非 `#[deprecated]`,不会在编译时提醒迁移。如要严格追踪,建议未来 Step 4 前统一加 `#[deprecated(note = "...")]`。
|
||||
3. **新加但未整体编译验证**:两个测试组均通过,但未执行 `cargo test -p mnote-web` 全量。局部风险较低。
|
||||
@@ -0,0 +1,42 @@
|
||||
# Batch C Worker C — Convex Export Productization Smoke
|
||||
|
||||
> 执行时间:2026-05-21
|
||||
> 执行者:Reasonix Code (Batch C Worker C)
|
||||
> 状态:`DONE`
|
||||
|
||||
## 背景
|
||||
|
||||
`design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md` 的 Gap C1
|
||||
已有 `--dry-run`、`--manifest`、`--conflict-report`、`--rollback` 能力,但缺独立 smoke 验证。
|
||||
|
||||
## 现有 smoke 检测
|
||||
|
||||
`scripts/task455-convex-export-plan-rollback-smoke.js` 已存在并覆盖全部三个行为:
|
||||
|
||||
| 行为 | 验证方式 | task455 对应逻辑 |
|
||||
|------|----------|------------------|
|
||||
| **dry-run** | `--dry-run` 后 pages 目录不存在;manifest 中 `dryRun: true` | `assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.md")), false)` |
|
||||
| **conflict** | 预写文件后 export 退出码 2;冲突报告含 `migration_conflict`;原内容未被覆盖 | `runExport([...], 2)` + `assert.equal(content, "原有内容\n")` |
|
||||
| **rollback** | export 后 rollback,pages/assets 均删除 | `assert.equal(fs.existsSync(...), false)` for pages + assets |
|
||||
|
||||
## 验证命令与结果
|
||||
|
||||
```bash
|
||||
node --check scripts/task455-convex-export-plan-rollback-smoke.js
|
||||
node scripts/task455-convex-export-plan-rollback-smoke.js
|
||||
```
|
||||
|
||||
**结果:** ✅ `{"ok":true,"smoke":"task455-convex-export-plan-rollback"}`
|
||||
|
||||
## 修改文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md` | Gap C1 标记 ✅ 完成;5.2 新增 task455 条目;5.3 三个 smoke 标记通过;6.3 总完成率 C1 行更新 |
|
||||
| `design/01-tree-first-graph-kernel/process/1-10-batch-c-p1-resource-buffer-gfm-convex-checklist-v1.md` | 新增 Worker C 执行记录 |
|
||||
|
||||
## 未完成项和风险
|
||||
|
||||
- **无未完成项** — dry-run/conflict/rollback 三个行为已有 `task455` 离线 smoke 覆盖。
|
||||
- **风险**:task455 使用 tmpdir,离线运行不依赖 Convex,无后续依赖风险。
|
||||
- **后续建议**:若有新的 export 行为变更(如增量导出、自定义路径映射),在 `task455` 基础上追加断言即可。
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# 1-10 [process] Batch C P1 Resource / BufferStore / GFM / Convex Evidence Checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
>
|
||||
> 阶段:Batch C / P1 小尾项收口
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在 Batch B 已完成 baseline smoke 复核后,继续处理 P1 中仍会干扰后续判断的尾项:
|
||||
|
||||
1. `5-26` resource open resolver 仍未实际收敛。
|
||||
2. `1-6` Gap A2 保存后刷新可见读回需要以真实 smoke 复核。
|
||||
3. `3-13` GFM AST 迁移仍有若干可小步关闭的测试 / 过渡标记尾项。
|
||||
4. `1-6` Gap C1 Convex export dry-run / conflict / rollback 仍缺独立 smoke 证据。
|
||||
|
||||
本批次不扩新产品面,不改 OnlyOffice 保存链路,不把 `documents/save` 重新升级为 local-first 长期正文入口。
|
||||
|
||||
## 2. Reasonix Worker 拆分
|
||||
|
||||
### Worker A:resource open resolver 最小收敛
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `scripts/task463-onlyoffice-resolver-smoke.js`
|
||||
|
||||
目标:
|
||||
|
||||
- 审查并最小收敛 `buildLocalOnlyOfficeOpenUrl`、`openEditorAttachmentEditTab`、`openEditorAttachmentNewWindow`、`openConvexAssetFromFileTree` 与 `resolveResourceOpen` 的重复 local Office / resource open 判断。
|
||||
- 若能安全实现,只改 `layout.rs` 与 `task463`。
|
||||
- 必须保持:正文附件“弹窗编辑”进 main resource tab,“新窗口编辑”进浏览器窗口,md/text/code 继续走 tiptap/text/code 渲染路径。
|
||||
|
||||
验收:
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell -- --test-threads=1`
|
||||
- `node --check scripts/task463-onlyoffice-resolver-smoke.js`
|
||||
- 如 smoke 可运行,执行 `node scripts/task463-onlyoffice-resolver-smoke.js`
|
||||
|
||||
### Worker B:GFM AST 尾项小包
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 补充或追认 GFM AST 尾项中低风险项:空表格单元格 / 降级策略测试、legacy->Tiptap marks 验证测试、写侧手写 Markdown 函数过渡标记。
|
||||
- 不修复 frontmatter title 优先级;该项是单独行为变更,保留给后续专项。
|
||||
- 不移除 web_shell legacy 适配分支,只允许加过渡注释或测试。
|
||||
|
||||
验收:
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown -- --test-threads=1`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web web_shell -- --test-threads=1`
|
||||
|
||||
### Worker C:Convex export 产品化补 smoke
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `scripts/task444-convex-workspace-export-local-fixture-smoke.js`
|
||||
- 可新增 `scripts/task48*-convex-export-*.js`
|
||||
|
||||
目标:
|
||||
|
||||
- 为 C1 已有能力补 smoke:dry-run 不写文件、同名冲突不覆盖、rollback 恢复导出前状态。
|
||||
- 不修改 Convex runtime,不改 local-first 架构口径。
|
||||
- 优先复用现有 fixture/export 脚本,新增脚本要可离线运行。
|
||||
|
||||
验收:
|
||||
|
||||
- `node --check scripts/task444-convex-workspace-export-local-fixture-smoke.js`
|
||||
- `node scripts/task444-convex-workspace-export-local-fixture-smoke.js`
|
||||
- 新增 smoke 的 `node --check` 与实跑通过。
|
||||
|
||||
## 3. Codex 本地关键路径
|
||||
|
||||
Codex 同步处理:
|
||||
|
||||
- A2 保存后刷新可见读回:复现 `POST /api/page-body/write` 后刷新 editor DOM 不显示正文的问题,判断是过期证据、hydration 问题,还是当前真实回归。
|
||||
- 若是 smoke 缺失,补最小 browser smoke;若是真回归,优先修复 web shell / aggregate bootstrap。
|
||||
|
||||
## 4. 完成条件
|
||||
|
||||
- Reasonix worker 均有 `final.md` / `result.json` 或明确 `TIMEOUT` 记录。
|
||||
- Codex 独立审查 diff,不直接相信 worker 总结。
|
||||
- 相关 targeted tests 与 browser smoke 实跑通过或形成明确 bug。
|
||||
- `1-6`、`1-9`、`3-13`、`5-26` 文档状态更新。
|
||||
- 提交前运行 `codegraph sync .`、`codegraph status`、`git diff --check`。
|
||||
|
||||
## 5. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Codex 建立 Batch C checklist,准备派发 Worker A/B/C,并本地排查 A2 刷新读回。
|
||||
- 2026-05-21(Codex):新增并实跑 `task484-local-folder-page-body-refresh-readback-smoke.js`,验证 `POST /api/page-body/write` 后磁盘、Page Aggregate、刷新后 ProseMirror 均读回新正文;`1-6` Gap A2 浏览器可见读回改判为完成,剩余仅为 conflict UI 数据源接 BufferStore。
|
||||
- 2026-05-21(Reasonix Batch C Worker A):worker 只提交 resource resolver 收敛计划,未生成 `final.md` / `result.json`,无业务代码 diff;记录为 `PLAN_ONLY_TIMEOUT`,`5-26` 保持 `process`。结果文件:`.codex/reasonix-tasks/results/batch-c-worker-a-resource-open-resolver-timeout.md`。
|
||||
- 2026-05-21(Reasonix Batch C Worker B):完成 GFM AST 尾项小包,补空引用块 / 空表格单元格测试、web_shell marks 断言、写侧 Markdown 过渡注释,并更新 `3-13`;结果文件:`.codex/reasonix-tasks/results/batch-c-worker-b-gfm-tail.md`。
|
||||
- 2026-05-21(Reasonix Batch C Worker C):`task455-convex-export-plan-rollback-smoke.js` 实跑通过,覆盖 dry-run/conflict/rollback 三个行为,`1-6` Gap C1 已可视为完成。结果文件:`.codex/reasonix-tasks/results/batch-c-worker-c-convex-export-smokes.md`。
|
||||
+11
-10
@@ -28,7 +28,7 @@
|
||||
- [x] **Gap A0(部分)**: local-first guard 已通过 ✅(`npm run check:local-first-convex-guard` → `{ok: true, guard: "local-first-convex"}`);browser smoke 尚未复跑。
|
||||
- [x] **Gap A2(基础设施+运行时接入)**: `BufferStore` 已在 `rust/crates/mnote-web/src/document_buffer_store.rs` 实现,并加入 `AppState.buffer_store`。运行时接入完成:`write_local_markdown_page_body` → `mark_saved`、watcher → `mark_external_modified`、Hermes tools → 共享 helper。11 个单测全部通过(含 3 个新集成测试)。
|
||||
- [x] **Gap A2(运行时接入—完成)**: `write_local_markdown_page_body` 已接入 `BufferStore.mark_saved()`;`documents.rs` `page_body_write` 和 `save` 路由已传递 `state.buffer_store`;Hermes `doc_apply_block_ops` 和 `page_command` 已传递 `state.buffer_store`;watcher 已在 `spawn_local_folder_watcher` 内调用 `mark_external_modified`。3 个新集成测试覆盖运行时入口。
|
||||
- [ ] **Gap A2(浏览器可见闭环)**: Codex 浏览器复核显示 `POST /api/page-body/write` 200、磁盘 `.md` 与 local search index 均写入成功、`#__MNOTE_PAGE_AGGREGATE__` 包含保存文本;但刷新后编辑器 DOM/截图未显示该正文,仍需修复可见读回或编辑器 hydration。
|
||||
- [x] **Gap A2(浏览器可见闭环)**: Codex 复核新增 `task484-local-folder-page-body-refresh-readback-smoke.js`,验证 `POST /api/page-body/write` 后磁盘 `.md`、Page Aggregate、刷新后的 ProseMirror 均读回新正文;旧的“刷新后 editor DOM 未显示正文”证据已判定为过期。
|
||||
- [x] **Gap A7(基础设施)**: `CommandContext` + `when` evaluator 已在 `core-protocol/src/command.rs` 实现。7 个单测全部通过(含 3 个必补测试)。
|
||||
- [ ] **Gap A7(UI/命令接入)**: File Tree 右键菜单、快捷键、AI 写入能力尚未消费 `CommandContext`。
|
||||
- [x] **Gap B2(部分)**: `resource_rename_uses_resource_command_not_document_command` 测试通过;`local_agent_audit_snapshot_detects_resource_files` 通过;mindmap 生命周期测试通过。
|
||||
@@ -88,7 +88,7 @@
|
||||
- [x] 接入外部文件变更 watcher:`spawn_local_folder_watcher` 已通过 `buffer_store.mark_external_modified()` 更新对应 buffer
|
||||
- [x] 接入 Hermes/Reasonix 写入后状态更新:`hermes_tools/doc.rs` 和 `hermes_tools/page.rs` 已通过共享 helper `write_local_markdown_page_body` 自动消费 BufferStore
|
||||
- [ ] conflict UI 数据源改为 buffer state:API 已就绪,未接入
|
||||
- [ ] 浏览器可见读回:保存后磁盘与 PageAggregate 已更新,但刷新后 tiptap/editor DOM 未显示保存正文
|
||||
- [x] 浏览器可见读回:`task484-local-folder-page-body-refresh-readback-smoke.js` 已验证保存后刷新 ProseMirror 可见正文不丢失
|
||||
|
||||
### 2.3 必补测试
|
||||
|
||||
@@ -109,11 +109,11 @@
|
||||
- [x] **已完成**:tiptap/保存路由/Hermes 写入链已通过 `write_local_markdown_page_body` 统一消费 `BufferStore`
|
||||
- [x] **已完成**:watcher 已通过 `spawn_local_folder_watcher` 调用 `mark_external_modified`
|
||||
- [ ] **未完成**:`documents/save` 的 conflict UI 数据源尚未改为 buffer state 查询
|
||||
- [ ] **未完成**:浏览器刷新后编辑器可见正文未读回;证据截图:`reasonix-a2-after-refresh-text-missing.png`
|
||||
- [x] **已完成**:浏览器刷新后编辑器可见正文读回;验证 `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task484-local-folder-page-body-refresh-readback-smoke.js`
|
||||
|
||||
### 2.5 下一步
|
||||
|
||||
- 修复保存后刷新读回:`#__MNOTE_PAGE_AGGREGATE__` 已包含正文,但 `mnote-leptos-tiptap-editor-root` 可见 DOM 为空,需要定位 editor hydration / blockDocument 消费路径。
|
||||
- 保存后刷新读回已由 `task484` 复核通过;后续若复现新问题,应以该 smoke 为回归入口。
|
||||
- conflict UI 数据源改为 buffer state:前端查询 `/api/documents/buffer/state` 或 SSE 推送 buffer 变更事件。
|
||||
|
||||
---
|
||||
@@ -193,7 +193,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 5. Gap C1:Convex 迁移产品化补证据 — 🔄 部分完成
|
||||
## 5. Gap C1:Convex 迁移产品化补证据 — ✅ 完成
|
||||
|
||||
### 5.1 当前代码锚点(已有未改)
|
||||
|
||||
@@ -210,13 +210,14 @@
|
||||
- [x] 实现同名冲突不覆盖 — 导出时不覆盖已有文件,输出冲突报告 ✅
|
||||
- [x] 实现 rollback — 支持 `--rollback` 参数 ✅
|
||||
- [x] 导出完成后重建 local index — task444 smoke 验证通过 ✅
|
||||
- [x] dry-run/conflict/rollback 独立 smoke — `task455-convex-export-plan-rollback-smoke.js` 三个行为全部通过 ✅
|
||||
|
||||
### 5.3 必补测试
|
||||
|
||||
- [x] `task444-convex-workspace-export-local-fixture-smoke.js` — 通过 ✅
|
||||
- [ ] 新增 smoke:dry run 不写文件 — `--dry-run` 已支持,需额外 smoke 验证
|
||||
- [ ] 新增 smoke:同名冲突不覆盖 — 已有冲突报告逻辑,需 smoke 验证
|
||||
- [ ] 新增 smoke:rollback 恢复导出前状态 — 已支持,需 smoke 验证
|
||||
- [x] 新增 smoke:dry run 不写文件 — `task455` 验证 `--dry-run` 后 pages 目录不存在 ✅
|
||||
- [x] 新增 smoke:同名冲突不覆盖 — `task455` 验证冲突后 exit(2) + 页面前内容保持 ✅
|
||||
- [x] 新增 smoke:rollback 恢复导出前状态 — `task455` 验证 rollback 后 pages/assets 均删除 ✅
|
||||
|
||||
---
|
||||
|
||||
@@ -285,7 +286,7 @@
|
||||
| Gap | 状态 | 关键产出 | 剩余 |
|
||||
|-----|------|----------|------|
|
||||
| A0 | 🔄 部分 | guard、task451、task452、task453 通过 | task431 等剩余 browser smokes 未复跑 |
|
||||
| A2 | 🔄 运行时接入完成,浏览器闭环未完成 | `BufferStore` 实现 + 11 tests;保存请求 200;磁盘和 PageAggregate 写入成功 | 刷新后 editor DOM 未显示正文;conflict UI 未接 buffer state |
|
||||
| A2 | 🔄 运行时接入 + 浏览器读回完成 | `BufferStore` 实现 + 11 tests;task484 验证保存后磁盘、PageAggregate、刷新后 ProseMirror 全部读回 | conflict UI 未接 buffer state |
|
||||
| A7 | 🔄 基础设施完成 | `CommandContext` + when evaluator + 7 tests | UI/快捷键/菜单接入 |
|
||||
| B2 | 🔄 部分 | 资源生命周期 + agent audit 测试通过;task443/task445 通过 | mindmap body 测试、跨文档 ResourceTab 400 噪声排查 |
|
||||
| C1 | 🔄 部分 | export/rollback/dry-run 脚本 + smoke 通过 | 额外 smoke 验证 |
|
||||
| C1 | ✅ 完成 | export/rollback/dry-run 脚本 + task444/task455 smoke 全部通过 | — |
|
||||
|
||||
@@ -362,6 +362,12 @@
|
||||
|
||||
完成后预期:正文真相、runtime projection、compat join 的边界更薄。
|
||||
|
||||
当前批次状态:
|
||||
|
||||
- `3-13` 的空引用块、空表格单元格、legacy marks、web_shell 过渡注释已由 Batch C 追认。
|
||||
- `1-6` 的 A2 浏览器可见读回、C1 export smoke 已由 Batch C 复核完成。
|
||||
- `5-26` 仍保持 process,等待最小可复核实现或新的 browser evidence。
|
||||
|
||||
### Batch D:live cache / command context
|
||||
|
||||
1. `3-3`
|
||||
|
||||
+5
@@ -118,6 +118,11 @@ Owner:
|
||||
- 审查 `3-13` 中 GFM AST 尾项:空段落/空表格、legacy <-> Tiptap mark、保存失败错误、过渡 parser 标记、web_shell 临时分支。
|
||||
- 给出下一步最小执行包,不把未验证项移动到 done。
|
||||
|
||||
## 5. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Batch B 仍保留为 process,原因是 `5-26` 还未形成可复核实现,A2 的冲突 UI 数据源接 BufferStore 也尚未落地。
|
||||
- 2026-05-21:`task443` / `task445` / `task452` 已在后续复核中修正为过期断言,不再作为当前业务回归证据。
|
||||
|
||||
验收:
|
||||
|
||||
- 输出明确表格:`已完成可追认` / `仍需代码修复` / `仅参考保留`。
|
||||
|
||||
+14
-10
@@ -294,7 +294,8 @@
|
||||
|
||||
- [x] 保证表格单元格内的 inline mark 不丢失。
|
||||
|
||||
- [ ] 保证空段落、空表格单元格、空引用块的处理规则固定。
|
||||
- [x] 保证空段落、空表格单元格、空引用块的处理规则固定。
|
||||
- 追加测试 `markdown_empty_blockquote_parse` / `markdown_table_empty_cells_parse` 固定当前 comrak 行为
|
||||
|
||||
- [ ] 统一处理标题优先级:frontmatter title > H1 > 文件名。
|
||||
|
||||
@@ -328,7 +329,8 @@
|
||||
|
||||
- [x] 确保 table cell 内的 inline mark 在读写两端都保留。
|
||||
|
||||
- [ ] 确保 legacy block -> Tiptap -> legacy block 不再丢 marks。
|
||||
- [x] 确保 legacy block -> Tiptap -> legacy block 不再丢 marks。
|
||||
- `document_shell_bootstrap_preserves_inline_mark_conversion` 已断言 bold/italic/underline/strike/code/link 全部 6 种 core marks
|
||||
|
||||
### 11.5 Task list 双向转换
|
||||
|
||||
@@ -376,7 +378,8 @@
|
||||
|
||||
- [x] 新增保存回写单测,验证 round-trip 不丢 task / marks。
|
||||
|
||||
- [ ] 新增 web shell 单测,验证 legacy <-> Tiptap 转换不丢 marks。
|
||||
- [x] 新增 web shell 单测,验证 legacy <-> Tiptap 转换不丢 marks。
|
||||
- `document_shell_bootstrap_preserves_inline_mark_conversion` 已扩展断言 bold/italic/underline/strike/code/link
|
||||
|
||||
- [x] 新增浏览器 smoke,验证 checkbox、code、strong、em、strike、link、table cell marks。
|
||||
|
||||
@@ -384,7 +387,8 @@
|
||||
|
||||
### 11.9 清理与迁移收尾
|
||||
|
||||
- [ ] 把手写 parser 标记为过渡实现。11
|
||||
- [x] 把手写 parser 标记为过渡实现。
|
||||
- `local_folder_source.rs` 中 `inline_nodes_to_markdown`、`editor_blocks_to_markdown_for_file`、`editor_blocks_to_markdown_with_rewrite` 均已添加中文过渡注释
|
||||
|
||||
- [x] 移除 `local_folder_source.rs` 中不再需要的临时解析函数。
|
||||
|
||||
@@ -421,11 +425,11 @@
|
||||
|
||||
| 项 | 状态 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.2 空段落/空表格单元格/空引用块 | **[ ] 保留** | 行为由 comrak 默认输出决定,无显式规则文档化。建议补单测固定当前行为即可,P1 不阻塞。 |
|
||||
| 11.4 legacy→Tiptap→legacy marks | **[ ] 保留** | 缺少反向转换集成测试。`document_shell_bootstrap_preserves_inline_mark_conversion` 仅验证函数存在性。 |
|
||||
| 11.6 列对齐/空单元格/带 mark 单元格降级策略测试 | **[ ] 部分完成** | 列对齐已有测试(应追认),但空单元格和降级策略仍无覆盖。 |
|
||||
| 11.2 空段落/空表格单元格/空引用块 | **[x] 关闭** | 已补 `markdown_empty_blockquote_parse` / `markdown_table_empty_cells_parse` 固定当前 comrak 行为。 |
|
||||
| 11.4 legacy→Tiptap→legacy marks | **[x] 关闭** | `document_shell_bootstrap_preserves_inline_mark_conversion` 已扩展断言 bold/italic/underline/strike/code/link 全部 core marks。 |
|
||||
| 11.6 列对齐/空单元格/带 mark 单元格降级策略测试 | **[ ] 部分完成** | 列对齐已追认,空单元格已补测试关闭。降级策略仍无覆盖。 |
|
||||
| 11.7 保存失败状态保留 | **[ ] 保留** | 冲突/写失败返回 WebError,前端错误处理和状态保留逻辑无测试。 |
|
||||
| 11.8 web shell 单测 | **[ ] 保留** | 缺少实际调用 legacyInlineContentToTiptap 并验证输出 marks 完整性的测试。 |
|
||||
| 11.9 手写 parser 标记为过渡 | **[ ] 保留** | `local_folder_source.rs` 中 `inline_nodes_to_markdown` / `editor_blocks_to_markdown_*` 写侧函数无 `#[deprecated]` 标注。 |
|
||||
| 11.9 web_shell.rs 临时适配分支移除 | **[ ] 保留** | JS 侧 `legacyStylesToTiptapMarks` 等适配函数仍存在。 |
|
||||
| 11.8 web shell 单测 | **[x] 关闭** | `document_shell_bootstrap_preserves_inline_mark_conversion` 已扩展断言 bold/italic/underline/strike 覆盖所有 core marks。 |
|
||||
| 11.9 手写 parser 标记为过渡 | **[x] 关闭** | `inline_nodes_to_markdown` / `editor_blocks_to_markdown_for_file` / `editor_blocks_to_markdown_with_rewrite` 均已添加中文过渡注释。 |
|
||||
| 11.9 web_shell.rs 临时适配分支移除 | **[ ] 保留** | JS 侧 `legacyStylesToTiptapMarks` 等适配函数仍存在(已加过渡注释 TODO(step-4))。 |
|
||||
| 11.9 process→done | **[ ] 保留** | 以上未完成项关闭后方可移动。
|
||||
|
||||
@@ -6218,6 +6218,9 @@ fn next_available_directory_path(directory: &Path, stem: &str) -> PathBuf {
|
||||
directory.join(format!("{stem}-{}", std::process::id()))
|
||||
}
|
||||
|
||||
// 过渡实现:写侧手写 Markdown 回写入口函数。当前通过 editor_blocks_to_markdown_with_rewrite
|
||||
// 将 editor block document 转为 Markdown 文本,AST 迁移 complete 后应替换为
|
||||
// AST→Markdown 反向映射(见 design/03-rust-web/process/3-13 方案 Step 3)。
|
||||
fn editor_blocks_to_markdown_for_file(
|
||||
content: &Value,
|
||||
root: &Path,
|
||||
@@ -6387,6 +6390,9 @@ fn stable_hash_hex(value: &str) -> String {
|
||||
format!("{:016x}", hasher.finish())
|
||||
}
|
||||
|
||||
// 过渡实现:写侧手写 Markdown 回写核心函数。逐 block 遍历,通过
|
||||
// inline_nodes_to_markdown 将行内节点转为 Markdown 文本。
|
||||
// AST 迁移 complete 后应替换为 block→Markdown AST→文本 的两次映射。
|
||||
fn editor_blocks_to_markdown_with_rewrite(
|
||||
content: &Value,
|
||||
local_file_context: Option<(&Path, &Path)>,
|
||||
@@ -6995,6 +7001,9 @@ fn block_content_value(block: &Value) -> Option<&Value> {
|
||||
block.get("content").or_else(|| block.get("contentNodes"))
|
||||
}
|
||||
|
||||
// 过渡实现:手写行内节点→Markdown 回写函数。从 editor block 的 content 数组中
|
||||
// 逐个节点提取 text + styles,按 legacy 样式格式输出为内联 Markdown。
|
||||
// AST + 中间 IR 迁移 complete 后应统一走 MarkdownInline→Markdown 反向映射。
|
||||
fn inline_nodes_to_markdown(value: &Value) -> String {
|
||||
if let Some(text) = value.as_str() {
|
||||
return escape_markdown_inline_text(text);
|
||||
|
||||
@@ -744,4 +744,72 @@ mod tests {
|
||||
assert_eq!(first["props"]["src"].as_str(), Some("assets/photo.jpg"));
|
||||
assert_eq!(first["props"]["alt"].as_str(), Some("示例图片"));
|
||||
}
|
||||
|
||||
/// 固定空引用块行为:`>` 在 GFM AST 中产生 BlockQuote 节点,
|
||||
/// collect_inline_children 返回空 vec → 输出 type=quote content=[]。
|
||||
#[test]
|
||||
fn markdown_empty_blockquote_parse() {
|
||||
let blocks = markdown_to_blocks("> \n\n>");
|
||||
let array = blocks.as_array().expect("blocks");
|
||||
assert!(!array.is_empty(), "应至少产生一个引用块");
|
||||
for block in array.iter() {
|
||||
if block["type"].as_str() == Some("quote") {
|
||||
let content = block["content"].as_array().expect("quote content");
|
||||
assert!(
|
||||
content.is_empty(),
|
||||
"空引用块 content 应为空,实际 {:?}",
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 固定空表格单元格行为:comrak 解析空单元格为空的 tiptap paragraph,
|
||||
/// content 字段为 []。
|
||||
#[test]
|
||||
fn markdown_table_empty_cells_parse() {
|
||||
let blocks = markdown_to_blocks("| 左 | |\n| --- | --- |\n| | 右 |\n");
|
||||
let array = blocks.as_array().expect("blocks");
|
||||
let table = array
|
||||
.iter()
|
||||
.find(|b| b["type"].as_str() == Some("table"))
|
||||
.expect("should have table block");
|
||||
let tiptap_table = table["props"]["tiptapTable"]
|
||||
.as_object()
|
||||
.expect("tiptapTable object");
|
||||
let content_rows = tiptap_table["content"]
|
||||
.as_array()
|
||||
.expect("table content rows");
|
||||
// 表头行:两个单元格
|
||||
let header_row = &content_rows[0]["content"].as_array().expect("header row cells");
|
||||
assert_eq!(header_row.len(), 2);
|
||||
// 表头第一个单元格(非空)
|
||||
let h1_cell = &header_row[0]["content"].as_array().expect("header cell paragraphs");
|
||||
assert!(!h1_cell.is_empty(), "表头第一个单元格不应为空");
|
||||
// 表头第二个单元格(空):单元格的 content 为 [{type:"paragraph", content:[]}]
|
||||
// 段落层的 content 应为空数组
|
||||
let h2_par_content = &header_row[1]["content"][0]["content"];
|
||||
let h2_inline = h2_par_content.as_array().expect("header cell para content");
|
||||
assert!(
|
||||
h2_inline.is_empty(),
|
||||
"空表头单元格的 paragraph content 应为空,实际值: {}",
|
||||
serde_json::to_string_pretty(h2_par_content).unwrap()
|
||||
);
|
||||
|
||||
// 数据行
|
||||
let data_row = &content_rows[1]["content"].as_array().expect("data row cells");
|
||||
assert_eq!(data_row.len(), 2);
|
||||
// 数据行第一个单元格(空):段落层的 content 应为空数组
|
||||
let d1_par_content = &data_row[0]["content"][0]["content"];
|
||||
let d1_inline = d1_par_content.as_array().expect("data cell para content");
|
||||
assert!(
|
||||
d1_inline.is_empty(),
|
||||
"空数据单元格的 paragraph content 应为空,实际值: {}",
|
||||
serde_json::to_string_pretty(d1_par_content).unwrap()
|
||||
);
|
||||
// 数据行第二个单元格(非空):段落层的 content 不应为空
|
||||
let d2_par_content = &data_row[1]["content"][0]["content"];
|
||||
let d2_inline = d2_par_content.as_array().expect("data cell para content");
|
||||
assert!(!d2_inline.is_empty(), "非空数据单元格不应为空");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,6 +828,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return '';
|
||||
};
|
||||
|
||||
// 过渡适配(TODO step-4):legacy→Tiptap inline marks 转换函数组。
|
||||
// AST/block 迁移 complete 后,前端应直接消费 block document 中的
|
||||
// tiptap 格式 marks(已由 Rust 侧 local_markdown_parser 输出),
|
||||
// 不再需要此 JS 侧样式→marks 适配层。
|
||||
const legacyStylesToTiptapMarks = (styles) => {
|
||||
if (!styles || typeof styles !== 'object') return [];
|
||||
const marks = [];
|
||||
@@ -5153,6 +5157,10 @@ mod tests {
|
||||
assert!(html.contains("legacyStylesToTiptapMarks"));
|
||||
assert!(html.contains("legacyMarkArrayToTiptapMarks"));
|
||||
assert!(html.contains("firstNonEmptyText(block?.props?.sourcePath"));
|
||||
assert!(html.contains("marks.push({ type: 'bold' })"));
|
||||
assert!(html.contains("marks.push({ type: 'italic' })"));
|
||||
assert!(html.contains("marks.push({ type: 'underline' })"));
|
||||
assert!(html.contains("marks.push({ type: 'strike' })"));
|
||||
assert!(html.contains("marks.push({ type: 'code' })"));
|
||||
assert!(html.contains("marks.push({ type: 'link', attrs: { href } })"));
|
||||
assert!(html.contains("styles.link = href"));
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task484-local-folder-page-body-refresh-readback-smoke");
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium"]
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
|
||||
function fileUrl(localPath) {
|
||||
return `file://${localPath}`;
|
||||
}
|
||||
|
||||
function localMdDocumentId(relativePath) {
|
||||
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||
}
|
||||
|
||||
function documentUrl(root, relativePath) {
|
||||
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function markdown(title, lines) {
|
||||
return [
|
||||
"---",
|
||||
`title: ${title}`,
|
||||
"---",
|
||||
"",
|
||||
...lines,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function writeWorkspaceManifest(root, ownerId) {
|
||||
const metadataDir = path.join(root, ".mnote");
|
||||
fs.mkdirSync(metadataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(metadataDir, "workspace.json"),
|
||||
`${JSON.stringify({
|
||||
workspaceId: `local-ws:${ownerId}:task484`,
|
||||
ownerId,
|
||||
createdAt: new Date().toISOString(),
|
||||
capabilities: ["local_files", "markdown_edit"],
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function openDocument(page, root, relativePath) {
|
||||
await page.goto(documentUrl(root, relativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function readEditorRuntime(page) {
|
||||
return page.evaluate(() => {
|
||||
const root = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
const aggregateNode = document.getElementById("__MNOTE_PAGE_AGGREGATE__");
|
||||
let aggregate = null;
|
||||
try {
|
||||
aggregate = JSON.parse(aggregateNode?.textContent || "null");
|
||||
} catch (_) {
|
||||
aggregate = null;
|
||||
}
|
||||
return {
|
||||
status: root?.getAttribute("data-runtime-editor-status") || "",
|
||||
error: root?.getAttribute("data-runtime-editor-error") || "",
|
||||
text: editor?.textContent || "",
|
||||
aggregateText: JSON.stringify(aggregate?.body || aggregate || {}),
|
||||
syncedAt: aggregateNode?.getAttribute("data-mnote-page-aggregate-synced-at") || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function pageBodyWrite(page, root, relativePath, expectedText) {
|
||||
const response = await page.evaluate(async ({ documentId, rootUri, text }) => {
|
||||
const body = {
|
||||
documentId,
|
||||
workspaceId: "local-ws:user_real:task484",
|
||||
sourceKind: "local_folder",
|
||||
rootUri,
|
||||
expectedFileVersion: null,
|
||||
contentFormat: "editorBlocks",
|
||||
editorSource: "task484-smoke",
|
||||
content: [
|
||||
{
|
||||
id: "block-task484-1",
|
||||
type: "paragraph",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
blockCount: 1,
|
||||
};
|
||||
const res = await fetch("/api/page-body/write", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
payload: await res.json().catch(() => null),
|
||||
};
|
||||
}, { documentId: localMdDocumentId(relativePath), rootUri: fileUrl(root), text: expectedText });
|
||||
assert.equal(response.ok, true, `page-body/write 应成功: ${JSON.stringify(response)}`);
|
||||
assert.equal(response.payload?.ok, true, `page-body/write payload 应 ok: ${JSON.stringify(response)}`);
|
||||
return response.payload.result;
|
||||
}
|
||||
|
||||
async function waitForEditorText(page, text) {
|
||||
await page.waitForFunction(
|
||||
(expected) => {
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
return (editor?.textContent || "").includes(expected);
|
||||
},
|
||||
text,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchPageAggregate(page, root, relativePath) {
|
||||
return page.evaluate(async ({ documentId, rootUri }) => {
|
||||
const url = new URL(`/api/page-aggregate/${encodeURIComponent(documentId)}`, window.location.origin);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", rootUri);
|
||||
const res = await fetch(url.toString(), { headers: { accept: "application/json" } });
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
payload: await res.json().catch(() => null),
|
||||
};
|
||||
}, { documentId: localMdDocumentId(relativePath), rootUri: fileUrl(root) });
|
||||
}
|
||||
|
||||
async function run() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task484-local-readback-"));
|
||||
const relativePath = "readback.md";
|
||||
const initialText = "initial task484 readback";
|
||||
const token = `task484-page-body-write-${Date.now()}`;
|
||||
writeWorkspaceManifest(root, "user_real");
|
||||
fs.writeFileSync(path.join(root, relativePath), markdown("Task 484 Readback", [initialText]), "utf8");
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 860 },
|
||||
extraHTTPHeaders: {
|
||||
"x-mnote-actor-id": "user_real",
|
||||
"x-mnote-actor-type": "user",
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const debug = { root, relativePath, token };
|
||||
|
||||
try {
|
||||
await openDocument(page, root, relativePath);
|
||||
await waitForEditorText(page, initialText);
|
||||
debug.before = await readEditorRuntime(page);
|
||||
|
||||
const saved = await pageBodyWrite(page, root, relativePath, token);
|
||||
debug.saved = saved;
|
||||
const disk = fs.readFileSync(path.join(root, relativePath), "utf8");
|
||||
assert(disk.includes(token), `磁盘应写入新正文: ${disk}`);
|
||||
|
||||
const aggregateBeforeReload = await fetchPageAggregate(page, root, relativePath);
|
||||
debug.aggregateBeforeReload = aggregateBeforeReload;
|
||||
assert(
|
||||
JSON.stringify(aggregateBeforeReload.payload || {}).includes(token),
|
||||
`刷新前 Page Aggregate 应读回新正文: ${JSON.stringify(aggregateBeforeReload)}`,
|
||||
);
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await waitForEditorText(page, token);
|
||||
debug.afterReload = await readEditorRuntime(page);
|
||||
|
||||
const aggregateAfterReload = await fetchPageAggregate(page, root, relativePath);
|
||||
debug.aggregateAfterReload = aggregateAfterReload;
|
||||
assert(
|
||||
JSON.stringify(aggregateAfterReload.payload || {}).includes(token),
|
||||
`刷新后 Page Aggregate 应读回新正文: ${JSON.stringify(aggregateAfterReload)}`,
|
||||
);
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
root,
|
||||
relativePath,
|
||||
token,
|
||||
saved,
|
||||
beforeText: debug.before.text,
|
||||
afterReloadText: debug.afterReload.text,
|
||||
resultPath: RESULT_PATH,
|
||||
};
|
||||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
console.log(`ok task484-local-folder-page-body-refresh-readback-smoke ${RESULT_PATH}`);
|
||||
} catch (error) {
|
||||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify({
|
||||
ok: false,
|
||||
error: String(error && error.stack || error),
|
||||
debug,
|
||||
}, null, 2)}\n`, "utf8");
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close().catch(() => {});
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user