Files
mnote/design/04-tree-domain/done/4-34-filetree-trash-dual-browser-no-refresh-v1.md
T

131 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 4-34 Filetree Trash Dual Browser No Refresh v1
> 状态:done
> 创建时间:2026-05-15
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 8
## 1. 目标
补齐文件树与垃圾箱的双浏览器 no-refresh 回归:A 浏览器执行页面、附件、mindmap、table 的 create / archive / restore / purge / empty-trashB 浏览器不刷新即可在 File Tree 与 Trash 工作台看到变化。
阶段 8 不以“单浏览器本地 optimistic update 成功”作为完成标准;`refreshTree()` 只能作为 A 浏览器本地兜底,不能作为 B 浏览器同步证据。
## 2. 验收矩阵
| 对象 | create | archive | restore | purge | empty-trash | 证据要求 |
| --- | --- | --- | --- | --- | --- | --- |
| 页面 document | A 新建后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空页面垃圾箱后 B Trash 清空 | 记录 tree delta/resync 或 Convex live 来源 |
| 普通附件 file asset | A 创建/上传或 seed 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 `tree.resource.*` / compat alias 与实时来源 |
| mindmap | A 新建 mindmap 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 mindmap route 仍为 compat alias |
| table | A 新建/seed table 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 table route 仍为 compat alias |
| local folder Markdown | 外部创建 `.md` 后 page/file tree 可见 | ✅ task475 API archivedelete 后进入 `.mnote/trash`trash-index 记录 `local-md:*` | ✅ task475 API restore`.md` 回原路径,trash 文件消失,index 清理) | ✅ task475 API purge(源路径与 trash 路径均不存在,index 清理) | ✅ task474 UI empty-trash(清空页面垃圾箱,confirm dialog → refresh(),无导航) | task475 覆盖 loose Markdown API trash lifecycletask474 覆盖 empty-trash UIbundled Markdown 由 Rust 单测覆盖 |
| local folder 非 md 资源 | ✅ task435 外部 create/rename/delete 无 reload | ✅ task437 API archive + task464 UI trash | ✅ task437 API restore + task473 UI restore no-refresh + focus/reveal | ✅ task437 API purge + task464 UI purge | ✅ task474 UI empty-trash(清空资源垃圾箱,confirm dialog → refresh(),无导航) | task435/437/464/473 覆盖;restore focus 缺口已关闭,见 task473task474 覆盖 empty-trash UI |
## 3. 分批执行
### 3.1 task432:页面生命周期双浏览器
先覆盖页面 document,因为它是 `tree.node.*` 主链:
- 使用测试账号或一次性用户登录两个 browser context。
- 使用隔离 workspace / 本轮测试前缀创建父页面与子页面。
- B 打开 3000 主入口文档页与 `/trash?workspaceId=...` 两个 tab,安装 tree event recorder。
- A 执行 create / archive / restore / purge / empty-trash。
- B 每一步不刷新页面,等待 File Tree / Trash DOM 更新,并记录是 `tree:delta``tree:resync`、EventSource snapshot 还是 Convex live 更新。
通过后才勾选阶段 8 的页面子项。
执行记录:
- 2026-05-15`task432-filetree-trash-page-dual-browser-no-refresh-smoke` 已通过。A 浏览器对页面执行 create / archive / restore / purge / empty-trashB 浏览器文件树与 `/trash` 均无需刷新可见更新,且全程无 `framenavigated`。事件来源记录为:create -> `tree:resync`archive -> `tree:delta remove_document`restore -> `tree:delta upsert_document`purge -> `tree:resync`empty-trash -> `tree:resync`。结果文件:`tmp/task432-filetree-trash-page-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15:清空页面垃圾箱的初次 smoke 暴露两层根因并已修正:Rust `documents.emptyTrashByWorkspace` 之前未写 `tree.trash.documents.emptied` domain eventConvex `documents.emptyTrashByWorkspace` validator 也未放行 `streamDeltaHint/domainEventHint/domainEventPlan`,导致 `/trash` 必须刷新才清空。两处已补齐并重新部署本地 Convex。
### 3.2 task433:普通附件生命周期双浏览器
覆盖普通 file asset
- 优先复用 `task428` / `task427` 的 API seed 方式,避免依赖上传文件选择器。
- archive / restore / purge 走 `/api/media/batch``/api/media/purge` 兼容 URL,但必须能在 artifact / response 中证明进入 `tree.resource.archive/restore/purge`
- B 侧分别观察 File Tree 与 Trash 工作台。
执行记录:
- 2026-05-15`task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke` 已通过。A 浏览器对普通附件执行 upload / archive / restore / archive+purge / upload+archive+empty-trashB 浏览器文件树与 `/trash` 均无需刷新可见更新,执行阶段无 `framenavigated`。事件来源记录为:upload -> `tree:delta upsert_assets`archive -> `tree:resync`restore -> `tree:resync`purge -> `tree:resync`empty-trash -> `tree:resync`。结果文件:`tmp/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15:初次 smoke 暴露 `tree.resource.archive/restore/purge` legacy Convex 参数转换覆盖真实 Convex userId,导致 `mediaAssets.patchById` membership 检查失败;已修正 `mnote-web` transport,使资源 lifecycle 转换优先保留 `args_json.userId`。随后暴露 `/api/media/empty-trash` 不写 tree event,导致 B `/trash` 清空必须刷新;已补 `tree.trash.media.emptied` domain event 与 `resync_required` stream delta。
- 2026-05-15:验证 `cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_resource_lifecycle_args_keep_effective_user_id -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web media_trash_routes_delete_restore_purge_and_empty -- --nocapture` 均通过。
### 3.3 task434mindmap / table 生命周期双浏览器
覆盖仍处 compat alias 的 mindmap / table
- mindmap 可复用现有 mindmap block / asset seed 方式。
- table 可复用 `task427` 的 table seed 与 purge query 方式。
- 本阶段不把 mindmap / table 宣称为正式 `tree.resource.*` cutover,只验证双浏览器可见性并保留 compat 边界。
执行记录:
- 2026-05-15`task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke` 已通过。A 浏览器对 mindmap 执行 create / archive / restore / archive+purge,对 table 执行 create / archive / restore / archive+purge,并对 mindmap + table 执行 create / archive / empty-trashB 浏览器文件树与 `/trash` 均无需刷新可见更新,执行阶段无 `framenavigated`。结果文件:`tmp/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15:为避免把 table 预置数据误标为 create 实时证据,Rust `mnote-web` 已新增 `/api/tables/create` 兼容入口,创建成功后记录 `tree.resource.table.created` domain event 与 `resync_required` stream delta。mindmap / table 的 delete / restore / purge / empty-trash compat route 也补齐 tree resync artifacts。
- 2026-05-15:事件来源记录为:mindmap create / archive / restore 主要触发 `tree:delta resync_required`mindmap purge 触发 `tree:resync`table create / archive / restore 主要触发 `tree:delta resync_required`table purge / empty-trash 触发 `tree:resync`
- 2026-05-15:验证 `cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1``node --check scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 均通过。
### 3.4 task435empty-trash 双浏览器汇总
在一次性 workspace 中执行页面垃圾箱与资源垃圾箱清空:
- A 清空页面垃圾箱后,B `/trash` 页面不刷新变为空。
- A 清空资源垃圾箱后,B `/trash` 页面资源区不刷新变为空。
- Convex query 断言对应对象不可再查询。
### 3.5 task436local folder watcher 与本地资源生命周期
本阶段不混入 Convex 双浏览器矩阵,单独验证 local folder source
- 打开 local folder page tree 与 filetree,记录初始 `navigationEvents`
- 外部创建 `.md`,断言 page tree / filetree 可见,并记录是否由 `/api/local-folder/events``/api/tree/local-folder-watch`、reload 或手动刷新驱动。
- 外部重命名 `.md`,断言旧 row 消失、新 row 出现;当前打开文档若受影响,应出现刷新或冲突提示。
- 外部删除 `.md`,断言 page tree / filetree 中 row 消失;若当前打开该文档,应有明确状态提示。
- 外部创建 / 重命名 / 删除至少两类非 md 文件,例如 `.txt``.png`,断言 filetree 与真实文件系统一致,并记录是否发生 `window.location.reload()`
- 对本地非 md 资源执行删除 / restore / purge;若 UI 仍禁用,记录为未完成能力,不得用 Markdown trash 代替。
当前只读审计结论:
- `LocalFolderWatcherRegistry` 使用 `notify` 递归监听 root,但事件发送前经过 `is_markdown_path`,只允许 `.md/.markdown`
- `/api/local-folder/events` 订阅该 watcher,主要服务打开的本地 Markdown document session。
- 主 tree 的 local folder 模式不接 `/api/tree/events`,而是轮询 `/api/tree/local-folder-watch``startLocalFolderSidebarWatch`1200ms 间隔)。revision 变化时调用 `refreshLocalFolderSidebarSnapshot()` 重渲染 filetree**不触发 `window.location.reload()`**。Rust 单元测试 `sidebar_tree_runtime_polls_local_folder_without_browser_reload` 验证了 SSR template 中不含 reload 语句。
- trash workbench 的 local folder restore/purge 使用 `refresh()`gateway.rs:1040-1060):`fetch` + `DOMParser` 替换 workbench `innerHTML`,不触发浏览器导航。但 restore/purge 完成后不调用 `refreshLocalFolderSidebarSnapshot()`,filetree 更新依赖下次轮询触发。
- `task163-local-folder-unified-tree-browser-smoke.js` 已覆盖外部新增 Markdown、外部新增 / 删除 `watcher-asset.txt` 的可见性;`task435-local-folder-watch-no-reload-smoke` 已覆盖 Markdown 与非 md 外部 create/rename/delete 无 reload;非 md trash/restore/purge 已由 task437API)和 task464UI)覆盖;local folder Markdown 自身 trash lifecyclearchive/restore/purge through tree commands)已由 task475 覆盖(API-onlyloose Markdown 的 delete → trash 进入/restore 恢复/purge 清理)。
- `task435-local-folder-watch-no-reload-smoke` 已通过:外部非 md 文件(`.txt``.png`)的 create / rename / delete 在 filetree 中无刷新可见更新,全程无 `framenavigated`。(结果:`tmp/task435-local-folder-watch-no-reload-smoke/result.json`
- `task437-local-folder-asset-trash-lifecycle-smoke` 已通过(API-only):`tree.resource.archive` / `restore` / `purge` 的 canonicalCommand 与文件系统一致性。(结果:`tmp/task437-local-folder-asset-trash-lifecycle-smoke/result.json`
- `task464-local-folder-resource-trash-ui-smoke` 已通过(Browser UI):trash 页中 delete→visible / restore→file restored / purge→file gone 完整 UI 流程。(结果:`tmp/task464-local-folder-resource-trash-ui-smoke/result.json`
- `task473-local-folder-trash-restore-no-refresh-focus-gap-smoke` 新增并已由 Codex + Reasonix Browser Worker 实跑:验证 trash workbench 的 restore 操作为 fetch 型 `refresh()``restoreNavFree.delta=0`,不触发浏览器导航),restore 后 filetree 通过轮询显示恢复行(`fileReappearsInFiletree=true`)。2026-05-21/22 Codex 修复并复跑后,恢复行出现 focus 证据(`data-active="true"`),focus gap 已关闭。(结果:`tmp/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke/result.json`;浏览器证据:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`
- `task475-local-folder-markdown-trash-lifecycle-smoke` 已通过(API-only):验证 loose Markdown 页面在 `/api/tree/commands` 的 deletearchive)→ restore → 再次 delete → purge 完整生命周期,文件系统与 `trash-index.json` 一致性。当前实现中 loose Markdown 的 delete/restore/purge 响应不含 `canonicalCommand` 字段(与 `trash_local_raw_file` 不同)。bundled Markdown 未覆盖(bundle 涉及目录整体移动/恢复,由 Rust 单测 `local_tree_command_manages_loose_markdown_like_regular_file``local_tree_command_delete_page_bundle_directory_uses_page_trash` 覆盖)。(结果:`tmp/task475-local-folder-markdown-trash-lifecycle-smoke/result.json`
- `task474-local-folder-empty-trash-ui-smoke` 新增:验证 local folder trash workbench 的清空页面垃圾箱(`local-empty-documents`)和清空资源垃圾箱(`local-empty-resources`)按钮操作。点击按钮后 accept confirm dialog`refresh()` 以 fetch+DOMParser 替换 innerHTML,不触发浏览器导航;断言对应 trash row 消失、`trash-index.json` 对应 entry 清理、trash 文件删除。结果写入 `tmp/task474-local-folder-empty-trash-ui-smoke/result.json`
- 2026-05-21Codex 在最新临时 `mnote-web` `http://127.0.0.1:3014` 上复跑 `task474` / `task475` 均通过;Reasonix Browser Worker 输出 `tmp/reasonix-batch-a3-browser-task474-empty-trash-2026-05-21/`,截图显示清空后页面与资源均为 0,两个清空按钮 disabled。
通过后才允许在 10-review 中把 local folder 阶段 8 子项勾选;如果仍发生 reload,只能标为“reload 型刷新可见”,不能标为 no-refresh。
## 4. 实时来源记录
每个 smoke 结果必须记录:
- B 浏览器是否接到 `/api/tree/events`
- B 浏览器是否接到 `tree:snapshot``tree:delta``tree:resync`
- B 侧 DOM 更新是否发生在无 reload / 无 framenavigated 的前提下。
- 是否依赖了 A 浏览器本地 `refreshTree()`;如果依赖,只能作为 A 本地补偿,不计入 B 实时证据。
## 5. 当前边界
- 3000 常驻进程可能是旧 `mnote-web` 二进制;修改 Rust 后做 smoke 时必须重启 3000,或用新编译的临时 3001 并在结果中明确记录。
- 资源生命周期中,普通附件 file asset 已进入正式 `tree.resource.*`mindmap / table 仍是 compat route,阶段 8 只验证 no-refresh,不改变 cutover 口径。
- local folder Markdown watcher 只有部分历史覆盖,且主 tree filetree 当前是 revision polling;非 md 本地资源文件 watcher / trash / restore / purge 已由 task435(外部 create/rename/delete)、task437API trash lifecycle)、task464trash UI)、task473restore no-refresh + focus/reveal)分项覆盖;local folder empty-trash UImd 页面 + 非 md 资源)已由 task474 覆盖;local folder Markdown loose page trash lifecycle 已由 task475API)覆盖;bundled Markdown 由 Rust 单测覆盖,尚未编写独立 smoke。
- `resync_required` delta 不是数据替换本身,必须伴随后续 snapshot / resync 或客户端主动拉取 snapshot;后续新增资源 route 时必须补事件合同回归。
## 6. 完成标准
- `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 8 的 Convex 页面、普通附件、mindmap、table checklist 全部有对应 smoke 证据。
- local folder 子项必须有 task435/437/464/473/474/475 或等价 smoke 证据;若仍发生 reload,保持未完成 no-refresh 状态。restore focus 已由 task473 关闭,不再视为 blocker。
- 每个 smoke 结果写入 `tmp/task43*-*/result.json`,并在 10-review 中记录命令、对象、实时来源和剩余边界。
- 不再用单浏览器 no-refresh、API purge 成功或 Convex query 成功替代双浏览器 UI no-refresh。