feat(tree): checkpoint resource lifecycle work
提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。 不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"use strict";
|
||||
|
||||
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 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("");
|
||||
|
||||
async function requestJson(request, path, init = {}) {
|
||||
const response = await request.fetch(`${BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.data !== undefined ? { "content-type": "application/json" } : {}),
|
||||
...(init.headers || {}),
|
||||
},
|
||||
timeout: 20_000,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = text;
|
||||
}
|
||||
if (!response.ok()) {
|
||||
throw new Error(`${path} 请求失败: ${response.status()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function convexCall(context, kind, path, args) {
|
||||
const cookies = await context.cookies(BASE_URL);
|
||||
const jwt = cookies.find((cookie) => cookie.name === "__convexAuthJWT")?.value;
|
||||
assert(jwt, "缺少 __convexAuthJWT cookie");
|
||||
const response = await fetch(`${CONVEX_URL}/api/${kind}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${jwt}`,
|
||||
"content-type": "application/json",
|
||||
"Convex-Client": "mnote-trash-empty-smoke",
|
||||
},
|
||||
body: JSON.stringify({ path, format: "convex_encoded_json", args: [args] }),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok || body.status !== "success") {
|
||||
throw new Error(`Convex ${kind} ${path} 失败: ${response.status} ${JSON.stringify(body)}`);
|
||||
}
|
||||
return body.value;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stamp = Date.now();
|
||||
const email = `mnote.trash.${stamp}@example.com`;
|
||||
const username = `trash-${stamp}`;
|
||||
const assetId = `asset_trash_${stamp}`;
|
||||
const mindmapId = `mind_trash_${stamp}`;
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const request = context.request;
|
||||
|
||||
try {
|
||||
await requestJson(request, "/api/auth", {
|
||||
method: "POST",
|
||||
data: {
|
||||
action: "auth:signIn",
|
||||
args: {
|
||||
provider: "password",
|
||||
params: { email, password: e2ePassword(), flow: "signUp", name: username },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
||||
const currentUser = await convexCall(context, "query", "users:currentUser", {});
|
||||
const userId = currentUser?._id;
|
||||
assert(userId, "缺少 Convex currentUser._id");
|
||||
const workspaces = await convexCall(context, "query", "workspaces:fetchWorkspaceSummaries", {});
|
||||
const workspaceId = workspaces.activeWorkspaceId;
|
||||
assert(workspaceId, "缺少隔离 workspaceId");
|
||||
|
||||
const root = await requestJson(request, "/api/tree/commands", {
|
||||
method: "POST",
|
||||
data: { action: "create", workspaceId, title: `TEST-10REVIEW-07-P3-root-${stamp}` },
|
||||
});
|
||||
const rootId = root.result.documentId;
|
||||
const child = await requestJson(request, "/api/tree/commands", {
|
||||
method: "POST",
|
||||
data: { action: "create", workspaceId, parentId: rootId, title: `TEST-10REVIEW-07-P3-child-${stamp}` },
|
||||
});
|
||||
const childId = child.result.documentId;
|
||||
const resourceDoc = await requestJson(request, "/api/tree/commands", {
|
||||
method: "POST",
|
||||
data: { action: "create", workspaceId, title: `TEST-10REVIEW-07-P3-resources-${stamp}` },
|
||||
});
|
||||
const resourceDocId = resourceDoc.result.documentId;
|
||||
|
||||
await convexCall(context, "mutation", "mediaAssets:create", {
|
||||
userId,
|
||||
asset: {
|
||||
id: assetId,
|
||||
workspace_id: workspaceId,
|
||||
document_id: resourceDocId,
|
||||
asset_type: "file",
|
||||
file_url: null,
|
||||
thumbnail_url: null,
|
||||
storage_id: null,
|
||||
bucket: null,
|
||||
storage_path: null,
|
||||
file_name: `TEST-10REVIEW-07-P3-file-${stamp}.txt`,
|
||||
file_size: 12,
|
||||
mime_type: "text/plain",
|
||||
},
|
||||
});
|
||||
await convexCall(context, "mutation", "mindmaps:put", {
|
||||
docId: resourceDocId,
|
||||
mindmapId,
|
||||
data: { root: { data: { text: `TEST-10REVIEW-07-P3-mind-${stamp}` }, children: [] } },
|
||||
createOnly: true,
|
||||
});
|
||||
const table = await convexCall(context, "mutation", "tables:create", {
|
||||
userId,
|
||||
workspaceId,
|
||||
documentId: resourceDocId,
|
||||
title: `TEST-10REVIEW-07-P3-table-${stamp}`,
|
||||
schema: {},
|
||||
snapshot: null,
|
||||
});
|
||||
const tableId = table.id;
|
||||
await convexCall(context, "mutation", "tables:update", {
|
||||
userId,
|
||||
tableId,
|
||||
rows: [{ cells: ["p3-row"] }],
|
||||
});
|
||||
|
||||
await requestJson(request, "/api/tree/commands", {
|
||||
method: "POST",
|
||||
data: { action: "archive", workspaceId, documentId: rootId },
|
||||
});
|
||||
await requestJson(request, "/api/media/batch", {
|
||||
method: "POST",
|
||||
data: { action: "delete", assetIds: [assetId] },
|
||||
});
|
||||
await requestJson(request, `/api/mindmap/${encodeURIComponent(resourceDocId)}/${encodeURIComponent(mindmapId)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
await requestJson(request, `/api/tables/${encodeURIComponent(tableId)}`, { method: "DELETE" });
|
||||
|
||||
const trashedDocs = await convexCall(context, "query", "documents:listTrashedByWorkspace", { workspaceId });
|
||||
assert(trashedDocs.some((doc) => doc.id === rootId), "根页面未进入垃圾箱");
|
||||
assert(trashedDocs.some((doc) => doc.id === childId), "子页面未级联进入垃圾箱");
|
||||
const mediaBefore = await convexCall(context, "query", "mediaAssets:getById", { userId, id: assetId });
|
||||
assert(mediaBefore?.deleted_at, "附件未进入垃圾箱");
|
||||
const mindmapsBefore = await convexCall(context, "query", "mindmaps:listByWorkspace", { workspaceId, includeDeleted: true });
|
||||
assert(mindmapsBefore.some((row) => row.mindmap_id === mindmapId && row.deleted_at), "mindmap 未进入垃圾箱");
|
||||
const tablesBefore = await convexCall(context, "query", "tables:listByWorkspaceForSearch", {
|
||||
userId,
|
||||
workspaceId,
|
||||
includeArchived: true,
|
||||
limit: 100,
|
||||
});
|
||||
assert(tablesBefore.some((row) => row.id === tableId && row.deleted_at), "table 未进入垃圾箱");
|
||||
|
||||
const pageEmpty = await requestJson(request, "/api/documents/empty-trash", { method: "POST", data: { workspaceId } });
|
||||
const mediaEmpty = await requestJson(request, "/api/media/empty-trash", { method: "POST", data: { workspaceId } });
|
||||
const mindmapEmpty = await requestJson(request, "/api/mindmap-trash/empty", { method: "POST", data: { workspaceId } });
|
||||
const tableEmpty = await requestJson(request, "/api/tables/empty-trash", { method: "POST", data: { workspaceId } });
|
||||
|
||||
assert.equal(await convexCall(context, "query", "documents:getMeta", { id: rootId }), null, "根页面 purge 后仍可查询");
|
||||
assert.equal(await convexCall(context, "query", "documents:getMeta", { id: childId }), null, "子页面 purge 后仍可查询");
|
||||
assert.equal(await convexCall(context, "query", "mediaAssets:getById", { userId, id: assetId }), null, "附件 purge 后仍可查询");
|
||||
const mindmapsAfter = await convexCall(context, "query", "mindmaps:listByWorkspace", { workspaceId, includeDeleted: true });
|
||||
assert(!mindmapsAfter.some((row) => row.mindmap_id === mindmapId), "mindmap purge 后仍存在");
|
||||
assert.equal(await convexCall(context, "query", "tables:get", { userId, tableId }), null, "table purge 后仍可查询");
|
||||
assert.equal((await convexCall(context, "query", "tables:getRows", { tableId })).length, 0, "table rows purge 后仍存在");
|
||||
|
||||
await requestJson(request, "/api/documents/purge", { method: "POST", data: { documentId: resourceDocId } }).catch(() => null);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
email,
|
||||
userId,
|
||||
workspaceId,
|
||||
rootId,
|
||||
childId,
|
||||
resourceDocId,
|
||||
assetId,
|
||||
mindmapId,
|
||||
tableId,
|
||||
pageEmpty: pageEmpty.result,
|
||||
mediaEmpty: mediaEmpty.result,
|
||||
mindmapEmpty: mindmapEmpty.result,
|
||||
tableEmpty: tableEmpty.result,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user