# Batch A3 Worker A：local folder empty-trash UI smoke

> 更新时间：2026-05-22
> 任务：`task474-local-folder-empty-trash-ui-smoke`

## 修改文件

| 文件 | 操作 |
|------|------|
| `scripts/task474-local-folder-empty-trash-ui-smoke.js` | 新增 |
| `design/04-tree-domain/process/4-34-filetree-trash-dual-browser-no-refresh-v1.md` | 更新验收矩阵、执行记录、边界和完成标准 |
| `.codex/reasonix-tasks/results/batch-a3-worker-a-empty-trash.md` | 本文件 |

## Smoke 覆盖内容

### 测试场景

- **临时 local folder root**：创建松散 Markdown 页面 (`docs/test-page.md`) 和非 md 资源 (`docs/resource.txt`)
- **归档到垃圾箱**：通过 `POST /api/tree/commands` 将两个条目都 `delete` 进入 `.mnote/trash/`
- **打开 trash 页**：`/trash?sourceKind=local_folder&rootUri=file://...`
- **清空页面垃圾箱**：
  - 点击 `data-trash-action="local-empty-documents"` 按钮
  - accept `window.confirm('清空后无法恢复，确定继续吗？')`
  - 断言：MD trash row 消失、资源行仍然存在、`trash-index.json` 中 MD entry 删除、trash 文件删除、按钮变为 disabled
  - 导航断言：全程 `framenavigated` delta = 0
- **清空资源垃圾箱**：
  - 点击 `data-trash-action="local-empty-resources"` 按钮
  - accept confirm dialog
  - 断言：资源 trash row 消失、`trash-index.json` 中资源 entry 删除、trash 文件删除、按钮变为 disabled
  - 导航断言：全程 `framenavigated` delta = 0
- **最终验证**：`trash-index.json` 为空、原始源文件未恢复

### 关键选择器

| 元素 | 选择器 |
|------|--------|
| Trash workbench | `[data-testid="mnote-trash-workbench"][data-trash-source-kind="local_folder"]` |
| 清空页面垃圾箱 | `[data-trash-action="local-empty-documents"]` |
| 清空资源垃圾箱 | `[data-trash-action="local-empty-resources"]` |
| MD trash row | `article[data-trash-row="local"][data-trash-entry-id="local-md:docs~2Ftest-page.md"]` |
| Asset trash row | `article[data-trash-row="local"][data-trash-entry-id="local-file:docs/resource.txt"]` |
| Confirm 文本 | `'清空后无法恢复，确定继续吗？'` |

### 数据流

```
postTreeCommand(delete, "local-md:docs~2Ftest-page.md")
  → tree.resource.archive → 文件移至 .mnote/trash/ → trash-index.json 记录
postTreeCommand(delete, "local-file:docs/resource.txt")
  → tree.resource.archive → 文件移至 .mnote/trash/ → trash-index.json 记录
→ 打开 /trash → 两行可见
→ 点击 empty-documents → confirm.accept → Promise.all(rows.map purge) → refresh() (fetch+DOMParser)
→ MD 行消失，按钮 disabled，trash-index 清理，trash 文件删除，framenavigated=0
→ 点击 empty-resources → confirm.accept → Promise.all(rows.map purge) → refresh()
→ 资源行消失，按钮 disabled，trash-index 为空，trash 文件删除，framenavigated=0
```

## 验证命令和结果

```bash
# 语法验证
node --check scripts/task474-local-folder-empty-trash-ui-smoke.js
# → exit 0

# Codex 复核：最新 mnote-web 临时端口 3014 + Chromium 实跑
MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3014 \
  PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable \
  node scripts/task474-local-folder-empty-trash-ui-smoke.js
# → exit 0
# → 写入 tmp/task474-local-folder-empty-trash-ui-smoke/result.json
```

Codex 复核结果：

- `result.json.ok=true`
- `emptyDocuments.navDelta=0`
- `emptyResources.navDelta=0`
- `emptyDocuments.mdEntryClean=true`
- `emptyResources.assetEntryClean=true`
- `steps.length=5`

Reasonix Browser Worker 也已独立复测，输出：

- `tmp/reasonix-batch-a3-browser-task474-empty-trash-2026-05-21/result.json`
- `tmp/reasonix-batch-a3-browser-task474-empty-trash-2026-05-21/trash-workbench-empty.png`
- `tmp/reasonix-batch-a3-browser-task474-empty-trash-2026-05-21/trash-workbench-ui.png`

## 更新到 4-34 的内容

1. **验收矩阵**：local folder Markdown 和 local folder 非 md 资源两行的 empty-trash 列已从 `❌ 未覆盖` 改为 `✅ task474 UI empty-trash`
2. **执行记录**：在 3.5 末尾追加 task474 的覆盖说明
3. **当前边界**：补充 task474 覆盖的 empty-trash 项
4. **完成标准**：local folder 子项增加 `474`

## 未完成项 / 风险

| # | 项 | 状态 | 说明 |
|---|-----|------|------|
| 1 | bundled Markdown empty-trash | ❌ 未覆盖 | task474 只验证了松散 `.md` 的 empty-trash；bundled Markdown page（目录含 `Page.md`）未覆盖。empty-trash 操作通过 `data-resource-kind="markdown"/"markdown_bundle"` 选择器选择行，理论上 bundled Markdown 也会被选中，但未实地验证 |
| 2 | 双浏览器 empty-trash 实时性 | ❌ 未验证 | task474 是单浏览器 UI smoke（A 浏览器操作 + 断言）。B 浏览器不刷新可见更新（Convex 双浏览器矩阵）未覆盖。当前 local folder trash 的 `refresh()` 只刷新 A 浏览器自身 DOM，`tree:delta resync_required` 事件不推送跨浏览器 |
| 3 | 3000 未重新编译 | ✅ 已处理 | Codex 已在最新临时端口 `3014` 实跑通过 |
| 4 | GitHub 资源空 trash 后按钮状态 | ✅ 已验证 | 脚本断言清空后按钮变为 disabled |
| 5 | 大文件 empty-trash | ❌ 未验证 | 脚本仅用了几 KB 的小文件；大 trash 数量的 `refresh()` 性能未覆盖 |
