diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index 3bdd5092..6879b257 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -147,3 +147,10 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 2026-05-25:Batch L Worker B `reasonix-2026-05-24T19-30-30-509Z-18f62949` 只读审计 `refreshLocalFolderSidebarSnapshot`;Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree diff 为空。 - 审计结论:`refreshLocalFolderSidebarSnapshot` 仍是 shell 编排,包含 workspace/root/document 状态、两条 projection fetch、render 后副作用和 watcher 标记,不应整体迁入 `filetree-runtime.js`。 - 下一刀建议:优先迁投影数据读取器层(如 `readProjection`、`readDatasetProjection`、`projectionItems`、row/id/title/file accessor、`groupRowsByParent`),或更保守地先迁未使用的 `replaceSidebarTreeFromDocument`;Codex 本轮不直接实现 3-20 代码,只记录候选。 +- 2026-05-25:Batch M 按用户纠正尝试并行派发 3 个 Reasonix 实现 worker,均使用独立 worktree;run `reasonix-2026-05-24T19-40-19-862Z-b3029108` / `reasonix-2026-05-24T19-40-19-873Z-4347ed16` 仅有 `prompt.md`、`memory-recall.json`、`reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`,不采纳。 + - 失败原因:Reasonix ACP 并发串线,Worker C transcript 将 B/C/D 三个任务混为一个选择题并要求人工选择;不满足本文件 handoff 合同。 + - 主控处置:不从 Batch M 合入任何 diff;改用 Batch N 的独立 worktree 子 agent worker 继续推进,仍由 Codex 复核 diff 和验证。 +- 2026-05-25:Batch N 已启动 2 个 3-20 独立 worktree worker:Worker C `/mnt/Data1T/mnote-worktrees/0525-n-worker-c-3-20-projection-helpers` 负责 filetree projection data helper 外置;Worker D `/mnt/Data1T/mnote-worktrees/0525-n-worker-d-3-20-task435-smoke-assertion` 负责 `task435` local-folder watch projection 断言。Codex 已复核两个 worktree diff 并选择性移植。 + - Worker C:`filetree-runtime.js` 新增并导出 `readProjection`、`readSidebarDataset`、`readDatasetProjection`、`projectionItems`、`hasProjectionItems`、`nodeIdOf`、`rowIdOf`、`parentIdOf`、`titleOf`、`fileWorkspaceRelativePath`、`groupRowsByParent`;`layout.rs` 对应 inline fallback 优先委托 `fileTreeRuntimeFunction(...)`,未迁 `refreshLocalFolderSidebarSnapshot` / `startLocalFolderSidebarWatch` / fetch / post-render shell 副作用 / HTML renderer。 + - Worker D:`task435-local-folder-watch-no-reload-smoke.js` 新增 `waitForLocalFolderWatchProjectionApplied`,每个外部文件变化步骤前清理旧标记,步骤内显式等待 `data-mnote-local-folder-watch-applied="projection"`,未放宽 no-reload 断言;Codex 同步把初始 `Stable Page` 等待修正为当前 file tree 标题 `stable`。 + - 已验证:`node --check rust/crates/mnote-web/browser/filetree-runtime.js`、`node --check scripts/task435-local-folder-watch-no-reload-smoke.js`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime_contains_row_accessor_helpers -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --test-threads=1`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task435-local-folder-watch-no-reload-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 diff --git a/rust/crates/mnote-web/browser/filetree-runtime.js b/rust/crates/mnote-web/browser/filetree-runtime.js index 5695d578..5b86993d 100644 --- a/rust/crates/mnote-web/browser/filetree-runtime.js +++ b/rust/crates/mnote-web/browser/filetree-runtime.js @@ -137,6 +137,83 @@ function defaultObjectIdentityAttr(identity) { } } +function readProjection(value) { + if (!value || typeof value !== 'object') return null; + if (value.result && typeof value.result === 'object') return value.result; + if (value.snapshot && value.snapshot.tree) return value.snapshot.tree; + if (value.data && value.data.tree) return value.data.tree; + if (value.tree && typeof value.tree === 'object') return value.tree; + return value; +} + +function readSidebarDataset(value) { + if (!value || typeof value !== 'object') return null; + if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset; + if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset; + if (value.dataset && typeof value.dataset === 'object') return value.dataset; + if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar; + return null; +} + +function readDatasetProjection(value, snakeCaseKey, camelCaseKey) { + var dataset = readSidebarDataset(value); + if (!dataset || typeof dataset !== 'object') return null; + if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey]; + if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey]; + return null; +} + +function projectionItems(projection) { + var resolved = readProjection(projection); + return resolved && Array.isArray(resolved.items) ? resolved.items : []; +} + +function hasProjectionItems(projection) { + var resolved = readProjection(projection); + return Boolean(resolved && Array.isArray(resolved.items)); +} + +function nodeIdOf(item) { + return String(item && (item.nodeId || item.id || item.documentId) || '').trim(); +} + +function rowIdOf(item) { + return String(item && (item.rowId || item.nodeId || item.id) || '').trim(); +} + +function parentIdOf(item) { + return String(item && (item.parentNodeId || item.parentId || '') || '').trim(); +} + +function titleOf(item) { + return String(item && item.title || '无标题').trim() || '无标题'; +} + +function fileWorkspaceRelativePath(item) { + var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {}; + var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {}; + var fromWorkspacePath = String(workspacePath.relativePath || '').trim(); + if (fromWorkspacePath) return fromWorkspacePath; + var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {}; + var source = extra.source && typeof extra.source === 'object' ? extra.source : {}; + var fromSource = String(source.relativePath || '').trim(); + if (fromSource) return fromSource; + return String(item && (item.relativePath || item.rootRelativePath) || '').trim(); +} + +function groupRowsByParent(rows) { + rows = Array.isArray(rows) ? rows : []; + var ids = new Set(rows.map(nodeIdOf).filter(Boolean)); + var grouped = new Map(); + rows.forEach(function(item) { + var parentId = parentIdOf(item); + if (!ids.has(parentId)) parentId = ''; + if (!grouped.has(parentId)) grouped.set(parentId, []); + grouped.get(parentId).push(item); + }); + return grouped; +} + function revealFileTreeRow(row) { if (!(row instanceof HTMLElement)) return; var node = row.closest('.tree-node'); @@ -263,6 +340,17 @@ function fileTreeUploadTargetFallback(detail, deps) { } window.__mnoteFileTreeRuntime = { + readProjection: readProjection, + readSidebarDataset: readSidebarDataset, + readDatasetProjection: readDatasetProjection, + projectionItems: projectionItems, + hasProjectionItems: hasProjectionItems, + nodeIdOf: nodeIdOf, + rowIdOf: rowIdOf, + parentIdOf: parentIdOf, + titleOf: titleOf, + fileWorkspaceRelativePath: fileWorkspaceRelativePath, + groupRowsByParent: groupRowsByParent, fileTreeRowKind: fileTreeRowKind, fileTreeRowDocumentId: fileTreeRowDocumentId, fileTreeRowAssetId: fileTreeRowAssetId, diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 56d04ec6..192cb864 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -2142,6 +2142,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function readProjection(value) { + var runtimeFn = fileTreeRuntimeFunction('readProjection'); + if (runtimeFn) return runtimeFn(value); if (!value || typeof value !== 'object') return null; if (value.result && typeof value.result === 'object') return value.result; if (value.snapshot && value.snapshot.tree) return value.snapshot.tree; @@ -2151,6 +2153,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function readSidebarDataset(value) { + var runtimeFn = fileTreeRuntimeFunction('readSidebarDataset'); + if (runtimeFn) return runtimeFn(value); if (!value || typeof value !== 'object') return null; if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset; if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset; @@ -2160,6 +2164,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function readDatasetProjection(value, snakeCaseKey, camelCaseKey) { + var runtimeFn = fileTreeRuntimeFunction('readDatasetProjection'); + if (runtimeFn) return runtimeFn(value, snakeCaseKey, camelCaseKey); var dataset = readSidebarDataset(value); if (!dataset || typeof dataset !== 'object') return null; if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey]; @@ -2172,32 +2178,46 @@ const SIDEBAR_TREE_JS: &str = r##" } function projectionItems(projection) { + var runtimeFn = fileTreeRuntimeFunction('projectionItems'); + if (runtimeFn) return runtimeFn(projection); var resolved = readProjection(projection); return resolved && Array.isArray(resolved.items) ? resolved.items : []; } function hasProjectionItems(projection) { + var runtimeFn = fileTreeRuntimeFunction('hasProjectionItems'); + if (runtimeFn) return runtimeFn(projection); var resolved = readProjection(projection); return Boolean(resolved && Array.isArray(resolved.items)); } function nodeIdOf(item) { + var runtimeFn = fileTreeRuntimeFunction('nodeIdOf'); + if (runtimeFn) return runtimeFn(item); return String(item && (item.nodeId || item.id || item.documentId) || '').trim(); } function rowIdOf(item) { + var runtimeFn = fileTreeRuntimeFunction('rowIdOf'); + if (runtimeFn) return runtimeFn(item); return String(item && (item.rowId || item.nodeId || item.id) || '').trim(); } function parentIdOf(item) { + var runtimeFn = fileTreeRuntimeFunction('parentIdOf'); + if (runtimeFn) return runtimeFn(item); return String(item && (item.parentNodeId || item.parentId || '') || '').trim(); } function titleOf(item) { + var runtimeFn = fileTreeRuntimeFunction('titleOf'); + if (runtimeFn) return runtimeFn(item); return String(item && item.title || '无标题').trim() || '无标题'; } function fileWorkspaceRelativePath(item) { + var runtimeFn = fileTreeRuntimeFunction('fileWorkspaceRelativePath'); + if (runtimeFn) return runtimeFn(item); var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {}; var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {}; var fromWorkspacePath = String(workspacePath.relativePath || '').trim(); @@ -2210,6 +2230,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function groupRowsByParent(rows) { + var runtimeFn = fileTreeRuntimeFunction('groupRowsByParent'); + if (runtimeFn) return runtimeFn(rows); var ids = new Set(rows.map(nodeIdOf).filter(Boolean)); var grouped = new Map(); rows.forEach(function(item) { @@ -11054,6 +11076,17 @@ mod tests { assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('revealFileTreeAssetRow')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('appendUploadedAssetRow')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('readProjection')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('readSidebarDataset')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('readDatasetProjection')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('projectionItems')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('hasProjectionItems')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('nodeIdOf')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('rowIdOf')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('parentIdOf')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('titleOf')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('groupRowsByParent')")); let document_id_body = js_function_body(SIDEBAR_TREE_JS, "fileTreeRowDocumentId"); assert!( document_id_body.contains("data-document-id") @@ -11077,7 +11110,19 @@ mod tests { assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("function appendUploadedAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetFallback")); + assert!(FILETREE_RUNTIME_JS.contains("function readProjection")); + assert!(FILETREE_RUNTIME_JS.contains("function readSidebarDataset")); + assert!(FILETREE_RUNTIME_JS.contains("function readDatasetProjection")); + assert!(FILETREE_RUNTIME_JS.contains("function projectionItems")); + assert!(FILETREE_RUNTIME_JS.contains("function hasProjectionItems")); + assert!(FILETREE_RUNTIME_JS.contains("function nodeIdOf")); + assert!(FILETREE_RUNTIME_JS.contains("function rowIdOf")); + assert!(FILETREE_RUNTIME_JS.contains("function parentIdOf")); + assert!(FILETREE_RUNTIME_JS.contains("function titleOf")); + assert!(FILETREE_RUNTIME_JS.contains("function fileWorkspaceRelativePath")); + assert!(FILETREE_RUNTIME_JS.contains("function groupRowsByParent")); assert!(FILETREE_RUNTIME_JS.contains("appendUploadedAssetRow: appendUploadedAssetRow")); + assert!(FILETREE_RUNTIME_JS.contains("groupRowsByParent: groupRowsByParent")); assert!(FILETREE_RUNTIME_JS .contains("uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')")); assert!(FILETREE_RUNTIME_JS diff --git a/scripts/task435-local-folder-watch-no-reload-smoke.js b/scripts/task435-local-folder-watch-no-reload-smoke.js index f1ab0dc6..a1e1e9b7 100644 --- a/scripts/task435-local-folder-watch-no-reload-smoke.js +++ b/scripts/task435-local-folder-watch-no-reload-smoke.js @@ -106,8 +106,25 @@ async function waitForPageTreeNodeGone(page, documentId) { ); } -async function runStep(label, navigationEvents, action) { +async function waitForLocalFolderWatchProjectionApplied(page) { + await page.waitForFunction( + () => document.documentElement.getAttribute("data-mnote-local-folder-watch-applied") === "projection", + { timeout: UI_TIMEOUT_MS }, + ); + const appliedValue = await page.evaluate( + () => document.documentElement.getAttribute("data-mnote-local-folder-watch-applied"), + ); + assert( + appliedValue === "projection", + `local_folder watch 应通过 projection 应用,实际 data-mnote-local-folder-watch-applied=${appliedValue}`, + ); +} + +async function runStep(page, label, navigationEvents, action) { const before = navigationEvents.length; + await page.evaluate(() => { + document.documentElement.removeAttribute("data-mnote-local-folder-watch-applied"); + }); await action(); const after = navigationEvents.length; assert(after === before, `${label} 不应触发浏览器导航或 reload,before=${before} after=${after}`); @@ -147,30 +164,33 @@ async function run() { await quickLogin(page); await page.goto(treeUrl(root, "page"), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await waitForVisibleText(page, "Local Root"); - await waitForVisibleText(page, "Stable Page"); + await waitForVisibleText(page, "stable"); await page.waitForResponse((response) => response.url().includes("/api/tree/local-folder-watch") && response.ok(), { timeout: UI_TIMEOUT_MS, }).catch(() => {}); await page.waitForTimeout(100); navigationEvents.length = 0; - steps.push(await runStep("Markdown 外部创建后 page tree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部创建后 page tree 原地更新", navigationEvents, async () => { fs.writeFileSync(path.join(root, "docs", "watcher-create.md"), "# Watcher Create\n", "utf8"); await waitForPageTreeNode(page, localMdDocumentId("docs/watcher-create.md")); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("Markdown 外部重命名后 page tree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部重命名后 page tree 原地更新", navigationEvents, async () => { fs.renameSync( path.join(root, "docs", "watcher-create.md"), path.join(root, "docs", "watcher-renamed.md"), ); await waitForPageTreeNode(page, localMdDocumentId("docs/watcher-renamed.md")); await waitForPageTreeNodeGone(page, localMdDocumentId("docs/watcher-create.md")); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("Markdown 外部删除后 page tree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部删除后 page tree 原地更新", navigationEvents, async () => { fs.rmSync(path.join(root, "docs", "watcher-renamed.md")); await waitForPageTreeNodeGone(page, localMdDocumentId("docs/watcher-renamed.md")); + await waitForLocalFolderWatchProjectionApplied(page); })); await page.goto(treeUrl(root, "filetree"), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); @@ -182,61 +202,70 @@ async function run() { await page.waitForTimeout(100); navigationEvents.length = 0; - steps.push(await runStep("Markdown 外部创建后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部创建后 filetree 原地更新", navigationEvents, async () => { fs.writeFileSync(path.join(root, "docs", "watcher-filetree-md.md"), "# Watcher Filetree Markdown\n", "utf8"); await waitForFileTreeRow(page, "local:markdown:docs/watcher-filetree-md.md"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("Markdown 外部重命名后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部重命名后 filetree 原地更新", navigationEvents, async () => { fs.renameSync( path.join(root, "docs", "watcher-filetree-md.md"), path.join(root, "docs", "watcher-filetree-md-renamed.md"), ); await waitForFileTreeRow(page, "local:markdown:docs/watcher-filetree-md-renamed.md"); await waitForFileTreeRowGone(page, "local:markdown:docs/watcher-filetree-md.md"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("Markdown 外部删除后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "Markdown 外部删除后 filetree 原地更新", navigationEvents, async () => { fs.rmSync(path.join(root, "docs", "watcher-filetree-md-renamed.md")); await waitForFileTreeRowGone(page, "local:markdown:docs/watcher-filetree-md-renamed.md"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("非 md 资源外部创建后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "非 md 资源外部创建后 filetree 原地更新", navigationEvents, async () => { fs.writeFileSync(path.join(root, "docs", "watcher-asset.txt"), "watcher asset", "utf8"); await waitForFileTreeRow(page, "local:asset:docs/watcher-asset.txt"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("非 md 资源外部重命名后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "非 md 资源外部重命名后 filetree 原地更新", navigationEvents, async () => { fs.renameSync( path.join(root, "docs", "watcher-asset.txt"), path.join(root, "docs", "watcher-asset-renamed.txt"), ); await waitForFileTreeRow(page, "local:asset:docs/watcher-asset-renamed.txt"); await waitForFileTreeRowGone(page, "local:asset:docs/watcher-asset.txt"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("非 md 资源外部删除后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "非 md 资源外部删除后 filetree 原地更新", navigationEvents, async () => { fs.rmSync(path.join(root, "docs", "watcher-asset-renamed.txt")); await waitForFileTreeRowGone(page, "local:asset:docs/watcher-asset-renamed.txt"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("第二类非 md 资源外部创建后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "第二类非 md 资源外部创建后 filetree 原地更新", navigationEvents, async () => { fs.writeFileSync(path.join(root, "docs", "watcher-image.png"), "png", "utf8"); await waitForFileTreeRow(page, "local:asset:docs/watcher-image.png"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("第二类非 md 资源外部重命名后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "第二类非 md 资源外部重命名后 filetree 原地更新", navigationEvents, async () => { fs.renameSync( path.join(root, "docs", "watcher-image.png"), path.join(root, "docs", "watcher-image-renamed.png"), ); await waitForFileTreeRow(page, "local:asset:docs/watcher-image-renamed.png"); await waitForFileTreeRowGone(page, "local:asset:docs/watcher-image.png"); + await waitForLocalFolderWatchProjectionApplied(page); })); - steps.push(await runStep("第二类非 md 资源外部删除后 filetree 原地更新", navigationEvents, async () => { + steps.push(await runStep(page, "第二类非 md 资源外部删除后 filetree 原地更新", navigationEvents, async () => { fs.rmSync(path.join(root, "docs", "watcher-image-renamed.png")); await waitForFileTreeRowGone(page, "local:asset:docs/watcher-image-renamed.png"); + await waitForLocalFolderWatchProjectionApplied(page); })); const result = {