From e246006c81959ffcf4fc55144da00700eb7e70a2 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Wed, 20 May 2026 11:32:07 +0800 Subject: [PATCH] docs: classify stale smoke paths --- scripts/TESTING_REFERENCE.md | 41 +++- ...k179-tree-create-delete-no-reload-smoke.js | 6 + ...27-trash-empty-isolated-workspace-smoke.js | 2 +- ...28-filetree-bulk-delete-selection-smoke.js | 2 +- ...rash-page-dual-browser-no-refresh-smoke.js | 2 +- ...ile-asset-dual-browser-no-refresh-smoke.js | 2 +- ...map-table-dual-browser-no-refresh-smoke.js | 2 +- ...ocal-folder-asset-trash-lifecycle-smoke.js | 194 +++++++----------- 8 files changed, 123 insertions(+), 128 deletions(-) diff --git a/scripts/TESTING_REFERENCE.md b/scripts/TESTING_REFERENCE.md index 74aa1c7e..fa655741 100644 --- a/scripts/TESTING_REFERENCE.md +++ b/scripts/TESTING_REFERENCE.md @@ -7,6 +7,42 @@ - `scripts/` 里的主力仍然是 `Playwright smoke + Node 断言`。 - 对 `mnote` 来说,最有价值的不是把所有测试都换成“浏览器插件式控制”,而是把已有 smoke 的结果变得更可看、更易复核。 - `/doko` 适合作为 smoke 之后的“真实页面直观复核层”,不是用来替代 smoke。 +- 2026-05-20 起,默认主入口固定按 `http://127.0.0.1:3000` 理解;历史记录里的 `3001` 只代表当时临时 mnote-web 实例,不再作为默认验收入口。 +- 历史 smoke 分三类维护:`current` 可直接作为当前主链证据,`compat` 只验证 Convex/control-plane/兼容链,`retired/debug` 不应进入默认回归,除非脚本自身要求显式环境变量。 + +## 0. 当前 smoke 状态清单 + +这一节用于避免历史脚本被误当作当前架构主链验收。 + +### 0.1 当前主链优先脚本 + +- 入口与认证:`task114-rust-web-gateway-entry-smoke.js`、`task159-auth-entry-smoke.js`、`task097-homepage-entry-smoke.js` +- Rust SSR 文档页 / Page Aggregate:`task110-page-title-single-truth-smoke.js`、`task-page-aggregate-body-sync-smoke.js`、`task-page-aggregate-options-sync-smoke.js`、`task-page-aggregate-refresh-persistence-smoke.js` +- local-first 本地工作区:`task164-desktop-hot-local-folder-main-entry-smoke.js`、`task166-local-first-managed-workspace-no-convex-smoke.js`、`task167-local-markdown-title-body-options-no-convex-smoke.js`、`task436-local-markdown-open-document-external-change-smoke.js`、`task443-local-markdown-asset-upload-smoke.js`、`task451-local-markdown-conflict-resolution-ui-smoke.js`、`task452-local-search-index-browser-smoke.js`、`task453-local-folder-page-ai-changed-files-smoke.js` +- tree realtime / live cache:`task446-tree-rename-dual-browser-live-smoke.js`、`task447-tree-move-order-dual-browser-live-smoke.js`、`task448-tree-resync-recovery-dual-browser-smoke.js`、`task449-tree-sse-reconnect-snapshot-recovery-smoke.js` +- 资源对象与 mindmap:`task455-local-folder-mindmap-clean-smoke.js`、`task456-resource-object-shell-sync-smoke.js`、`task166-mindmap-phase6-block-smoke.js`、`task167-mindmap-kmind-parity-smoke.js`、`task168-mindmap-put-validator-smoke.js` +- local resource lifecycle API:`task437-local-folder-asset-trash-lifecycle-smoke.js`,现在只验证 `local-file:*` delete / restore / purge,不再依赖普通 `.txt` 是否出现在 UI 文件树。 + +### 0.2 兼容/control-plane 脚本 + +这些脚本仍可用于 Convex/control-plane 或 cloud-source 兼容链,但不能单独证明 local-first 主链正确: + +- `task427-trash-empty-isolated-workspace-smoke.js` +- `task428-filetree-bulk-delete-selection-smoke.js` +- `task432-filetree-trash-page-dual-browser-no-refresh-smoke.js` +- `task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js` +- `task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` +- `task444-convex-workspace-export-local-fixture-smoke.js` +- `task455-convex-export-plan-rollback-smoke.js` + +注意:上述 427 / 428 / 432 / 433 / 434 脚本默认入口已改回 `3000`;如需对临时端口验证,显式设置 `MNOTE_UI_BASE_URL`。 + +### 0.3 需要退役或拆分的历史脚本 + +- `task179-tree-create-delete-no-reload-smoke.js`:覆盖 `/tree` debug shell。`/tree` 已是显式 debug/internal 边界,脚本默认阻断;只有设置 `MNOTE_ALLOW_DEBUG_TREE_SMOKE=1` 时才可执行。 +- `task163-local-folder-unified-tree-browser-smoke.js`:历史“大一统”脚本,同时混合 local folder、Convex、拖拽、复制粘贴、watcher 和文件树行为。后续不要作为默认回归入口;优先用 164 / 166 / 436 / 441 / 443 / 451 / 452 / 455 这些聚焦脚本替代。 +- `task123-rust-web-tree-live-stream-consumer-smoke.js`:只证明 `/api/tree/events` SSE fallback 仍可用。当前 realtime 主链是 WebSocket push + SSE fallback;验证主链时优先使用 446 / 447 / 448,只有排查 fallback 时再跑 123。 +- 旧 `task019`、`task021`、`task022` 这类早期 UI regression 脚本只作为历史对照;后续默认不要用于当前 Rust SSR / local-first 主路径验收。 ## 1. 当前测试分层 @@ -39,9 +75,11 @@ - `task112-tree-rust-family-regression-smoke.js` - `task120-rust-web-tree-integration-smoke.js` - `task122-rust-web-create-page-ui-smoke.js` -- `task163-local-folder-unified-tree-browser-smoke.js` - `task164-page-options-visible-effect-smoke.js` - `task165-rust-web-dual-pane-smoke.js` +- `task164-desktop-hot-local-folder-main-entry-smoke.js` +- `task166-local-first-managed-workspace-no-convex-smoke.js` +- `task455-local-folder-mindmap-clean-smoke.js` 这一层已经覆盖了当前主线里最重要的部分: @@ -120,6 +158,7 @@ node scripts/task097-homepage-entry-smoke.js - tree / sidebar 改动:`task112`、`task120`、`task122` - document shell / island:`task110`、`task121` +- local-first:优先使用 `task164-desktop-hot-local-folder-main-entry-smoke.js`、`task166-local-first-managed-workspace-no-convex-smoke.js`、`task167-local-markdown-title-body-options-no-convex-smoke.js`、`task455-local-folder-mindmap-clean-smoke.js` - 页面设置:`task160`、`task164-page-options-visible-effect-smoke.js` - 双栏:`task165` - AI:优先使用 `task-hermes-page-ai-retirement-guard.js`、`task-page-block-ai-tools-smoke.js` 和当前 Hermes 页面 AI smoke;`task155`、`task156`、`task161` 等旧 `/api/ai-agent/run` smoke 只保留在本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/` 做历史对照 diff --git a/scripts/task179-tree-create-delete-no-reload-smoke.js b/scripts/task179-tree-create-delete-no-reload-smoke.js index 5594292c..2d4fb920 100644 --- a/scripts/task179-tree-create-delete-no-reload-smoke.js +++ b/scripts/task179-tree-create-delete-no-reload-smoke.js @@ -17,6 +17,12 @@ const TASK = "task179-tree-create-delete-no-reload-smoke"; const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); +if (process.env.MNOTE_ALLOW_DEBUG_TREE_SMOKE !== "1") { + throw new Error( + "task179 只覆盖已退为显式 debug/internal 的 /tree 壳。当前主链请使用 document/sidebar smoke;如确需复核 debug 壳,请设置 MNOTE_ALLOW_DEBUG_TREE_SMOKE=1。", + ); +} + async function writeResult(payload) { await fs.mkdir(OUTPUT_DIR, { recursive: true }); await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8"); diff --git a/scripts/task427-trash-empty-isolated-workspace-smoke.js b/scripts/task427-trash-empty-isolated-workspace-smoke.js index 9486d231..c8451bfa 100644 --- a/scripts/task427-trash-empty-isolated-workspace-smoke.js +++ b/scripts/task427-trash-empty-isolated-workspace-smoke.js @@ -3,7 +3,7 @@ const assert = require("node:assert/strict"); const { chromium } = require("playwright"); -const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3001").replace(/\/+$/, ""); +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const CONVEX_URL = (process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_SELF_HOSTED_URL || "http://127.0.0.1:3210").replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); // 说明:该 smoke 覆盖 convex-source trash 兼容路径,不代表 local-first 默认文件回收策略。 diff --git a/scripts/task428-filetree-bulk-delete-selection-smoke.js b/scripts/task428-filetree-bulk-delete-selection-smoke.js index 39c0b063..cf9aaa2e 100644 --- a/scripts/task428-filetree-bulk-delete-selection-smoke.js +++ b/scripts/task428-filetree-bulk-delete-selection-smoke.js @@ -7,7 +7,7 @@ const path = require("node:path"); const { chromium } = require("playwright"); const TASK = "task428-filetree-bulk-delete-selection-smoke"; -const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3001").replace(/\/+$/, ""); +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const CONVEX_URL = (process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_SELF_HOSTED_URL || "http://127.0.0.1:3210").replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); // 说明:该 smoke 覆盖 convex-source 批量删除兼容路径,不代表 local-first 默认 File Tree 主链。 diff --git a/scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js b/scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js index 06deff41..08de18c7 100644 --- a/scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js +++ b/scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js @@ -7,7 +7,7 @@ const path = require("node:path"); const { chromium } = require("playwright"); const TASK = "task432-filetree-trash-page-dual-browser-no-refresh-smoke"; -const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3001").replace(/\/+$/, ""); +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const AUTH_BASE_URL = (process.env.MNOTE_AUTH_BASE_URL || BASE_URL).replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 35_000); diff --git a/scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js b/scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js index 073117be..964ff0d1 100644 --- a/scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js +++ b/scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js @@ -7,7 +7,7 @@ const path = require("node:path"); const { chromium } = require("playwright"); const TASK = "task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke"; -const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3001").replace(/\/+$/, ""); +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const AUTH_BASE_URL = (process.env.MNOTE_AUTH_BASE_URL || BASE_URL).replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); // 说明:该 smoke 仍在验证 convex-source 的 file asset trash 兼容链路,不是 local-first 默认资源流。 diff --git a/scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js b/scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js index 4a46fc7d..1283663f 100644 --- a/scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js +++ b/scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js @@ -7,7 +7,7 @@ const path = require("node:path"); const { chromium } = require("playwright"); const TASK = "task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke"; -const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3001").replace(/\/+$/, ""); +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const AUTH_BASE_URL = (process.env.MNOTE_AUTH_BASE_URL || BASE_URL).replace(/\/+$/, ""); const CONVEX_URL = (process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_SELF_HOSTED_URL || "http://127.0.0.1:3210").replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); diff --git a/scripts/task437-local-folder-asset-trash-lifecycle-smoke.js b/scripts/task437-local-folder-asset-trash-lifecycle-smoke.js index c6db4930..e32a56b4 100644 --- a/scripts/task437-local-folder-asset-trash-lifecycle-smoke.js +++ b/scripts/task437-local-folder-asset-trash-lifecycle-smoke.js @@ -1,13 +1,11 @@ "use strict"; -const assert = require("node:assert"); +const assert = require("node:assert/strict"); 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", "task437-local-folder-asset-trash-lifecycle-smoke"); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); @@ -15,162 +13,114 @@ function fileUrl(localPath) { return `file://${localPath}`; } -function treeUrl(root) { - const url = new URL(`${BASE_URL}/`); - url.searchParams.set("treeView", "filetree"); - url.searchParams.set("sourceKind", "local_folder"); - url.searchParams.set("rootUri", fileUrl(root)); - return url.toString(); +function writeWorkspaceManifest(root) { + const metadataDir = path.join(root, ".mnote"); + fs.mkdirSync(metadataDir, { recursive: true }); + fs.writeFileSync( + path.join(metadataDir, "workspace.json"), + `${JSON.stringify({ + workspaceId: "local-ws:user_real:task437", + ownerId: "user_real", + createdAt: new Date().toISOString(), + capabilities: ["local_files", "tree_commands", "markdown_edit", "asset_upload"], + }, null, 2)}\n`, + "utf8", + ); } -function commandUrl() { - return `${BASE_URL}/api/tree/commands`; -} - -async function waitForFileTreeRow(page, rowId) { - await page.locator(`.tree-row[data-row-id="${rowId}"]`).waitFor({ - state: "visible", - timeout: UI_TIMEOUT_MS, +async function postTreeCommand(payload) { + const response = await fetch(`${BASE_URL}/api/tree/commands`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-mnote-actor-id": "user_real", + "x-mnote-actor-type": "user", + }, + body: JSON.stringify(payload), }); + const json = await response.json().catch(async () => ({ text: await response.text() })); + assert.equal(response.status, 200, `${payload.action} failed: ${JSON.stringify(json)}`); + return json; } -async function waitForFileTreeRowGone(page, rowId) { - await page.locator(`.tree-row[data-row-id="${rowId}"]`).waitFor({ - state: "detached", - timeout: UI_TIMEOUT_MS, - }); -} - -async function postTreeCommand(page, payload) { - return await page.evaluate(async ({ url, body }) => { - const response = await fetch(url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); - const json = await response.json().catch(() => null); - return { status: response.status, json }; - }, { url: commandUrl(), body: payload }); -} - -async function quickLogin(page) { - await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" }); - if (await quickLoginButton.count()) { - await quickLoginButton.click({ timeout: UI_TIMEOUT_MS }); - await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS }); - } -} - -async function runStep(label, navigationEvents, action) { - const before = navigationEvents.length; +async function runStep(label, action) { const detail = await action(); - const after = navigationEvents.length; - assert.equal(after, before, `${label} 不应触发浏览器导航或 reload`); - return { - label, - navigationEventsBefore: before, - navigationEventsAfter: after, - detail: detail || null, - }; + return { label, detail: detail || null }; } async function run() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-local-asset-trash-smoke-")); fs.mkdirSync(path.join(root, "docs"), { recursive: true }); + writeWorkspaceManifest(root); fs.writeFileSync(path.join(root, "README.md"), "# Local Root\n", "utf8"); fs.writeFileSync(path.join(root, "docs", "asset.txt"), "asset body", "utf8"); - const browser = await chromium.launch({ headless: true }); - 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 navigationEvents = []; - page.on("framenavigated", (frame) => { - if (frame === page.mainFrame()) { - navigationEvents.push({ url: frame.url(), timestamp: Date.now() }); - } - }); - - const rowId = "local:asset:docs/asset.txt"; const rootUri = fileUrl(root); + const assetId = "local-file:docs/asset.txt"; + const sourcePath = path.join(root, "docs", "asset.txt"); + const trashPath = path.join(root, ".mnote", "trash", "asset.txt"); + const trashIndexPath = path.join(root, ".mnote", "trash-index.json"); const steps = []; try { - page.on("dialog", async (dialog) => { - if (dialog.type() === "confirm") await dialog.accept(); - else await dialog.dismiss().catch(() => {}); - }); - await quickLogin(page); - await page.goto(treeUrl(root), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - await waitForFileTreeRow(page, rowId); - navigationEvents.length = 0; - - steps.push(await runStep("local asset Delete 进入本地回收站", navigationEvents, async () => { - await page.locator(`.tree-row[data-row-id="${rowId}"]`).click({ timeout: UI_TIMEOUT_MS }); - await page.keyboard.press("Delete"); - await waitForFileTreeRowGone(page, rowId); - assert(!fs.existsSync(path.join(root, "docs", "asset.txt")), "Delete 后源文件应消失"); - assert(fs.existsSync(path.join(root, ".mnote", "trash", "asset.txt")), "Delete 后文件应进入 .mnote/trash"); - const index = fs.readFileSync(path.join(root, ".mnote", "trash-index.json"), "utf8"); - assert(index.includes("local-file:docs/asset.txt"), "trash index 应记录 local_file entry"); - return { trashIndexHasLocalFile: true }; - })); - - steps.push(await runStep("local asset restore 回原路径", navigationEvents, async () => { - const result = await postTreeCommand(page, { - action: "restore", - sourceKind: "local_folder", - rootUri, - documentId: rowId, - }); - assert.equal(result.status, 200, `restore failed: ${JSON.stringify(result)}`); - assert.equal(result.json?.result?.execution?.canonicalCommand, "tree.resource.restore"); - await waitForFileTreeRow(page, rowId); - assert(fs.existsSync(path.join(root, "docs", "asset.txt")), "restore 后源文件应恢复"); - return result.json?.result?.execution || null; - })); - - steps.push(await runStep("local asset purge 清理 trash 文件与索引", navigationEvents, async () => { - let result = await postTreeCommand(page, { + steps.push(await runStep("local-file delete 进入本地回收站", async () => { + const result = await postTreeCommand({ action: "delete", sourceKind: "local_folder", rootUri, - documentId: rowId, + documentId: assetId, }); - assert.equal(result.status, 200, `delete before purge failed: ${JSON.stringify(result)}`); - await waitForFileTreeRowGone(page, rowId); - result = await postTreeCommand(page, { + assert.equal(result.result?.execution?.canonicalCommand, "tree.resource.archive"); + assert.equal(fs.existsSync(sourcePath), false, "Delete 后源文件应消失"); + assert.equal(fs.existsSync(trashPath), true, "Delete 后文件应进入 .mnote/trash"); + const index = fs.readFileSync(trashIndexPath, "utf8"); + assert.match(index, /local-file:docs\/asset\.txt/, "trash index 应记录 local-file entry"); + return result.result?.execution || null; + })); + + steps.push(await runStep("local-file restore 回原路径", async () => { + const result = await postTreeCommand({ + action: "restore", + sourceKind: "local_folder", + rootUri, + documentId: assetId, + }); + assert.equal(result.result?.execution?.canonicalCommand, "tree.resource.restore"); + assert.equal(fs.existsSync(sourcePath), true, "restore 后源文件应恢复"); + return result.result?.execution || null; + })); + + steps.push(await runStep("local-file purge 清理 trash 文件与索引", async () => { + await postTreeCommand({ + action: "delete", + sourceKind: "local_folder", + rootUri, + documentId: assetId, + }); + const result = await postTreeCommand({ action: "purge", sourceKind: "local_folder", rootUri, - documentId: rowId, + documentId: assetId, }); - assert.equal(result.status, 200, `purge failed: ${JSON.stringify(result)}`); - assert.equal(result.json?.result?.execution?.canonicalCommand, "tree.resource.purge"); - assert(!fs.existsSync(path.join(root, "docs", "asset.txt")), "purge 后源文件不应存在"); - assert(!fs.existsSync(path.join(root, ".mnote", "trash", "asset.txt")), "purge 后 trash 文件不应存在"); - const index = fs.readFileSync(path.join(root, ".mnote", "trash-index.json"), "utf8"); - assert(!index.includes("local-file:docs/asset.txt"), "purge 后 trash index 应清理 entry"); - return result.json?.result?.execution || null; + assert.equal(result.result?.execution?.canonicalCommand, "tree.resource.purge"); + assert.equal(fs.existsSync(sourcePath), false, "purge 后源文件不应存在"); + assert.equal(fs.existsSync(trashPath), false, "purge 后 trash 文件不应存在"); + const index = fs.readFileSync(trashIndexPath, "utf8"); + assert.doesNotMatch(index, /local-file:docs\/asset\.txt/, "purge 后 trash index 应清理 entry"); + return result.result?.execution || null; })); const result = { ok: true, baseUrl: BASE_URL, root, + assetId, steps, - navigationEvents, }; fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8"); console.log(`task437 local folder asset trash lifecycle smoke passed: ${RESULT_PATH}`); } finally { - await browser.close().catch(() => {}); fs.rmSync(root, { recursive: true, force: true }); } }