Files
mnote/scripts/task475-local-folder-markdown-trash-lifecycle-smoke.js

204 lines
8.4 KiB
JavaScript
Raw Permalink 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.
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task475-local-folder-markdown-trash-lifecycle-smoke");
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
function fileUrl(localPath) {
return `file://${localPath}`;
}
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:task475",
ownerId: "user_real",
createdAt: new Date().toISOString(),
capabilities: ["local_files", "tree_commands", "markdown_edit"],
}, null, 2)}\n`,
"utf8",
);
}
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 runStep(label, action) {
const detail = await action();
return { label, detail: detail || null };
}
async function run() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-local-md-trash-smoke-"));
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
writeWorkspaceManifest(root);
fs.writeFileSync(path.join(root, "README.md"), "# Local Root\n", "utf8");
// 创建 loose Markdown 页面(非 bundle
const mdFileName = "test-page.md";
const mdContent = "# Test Page\n\nThis is a test Markdown page for trash lifecycle.\n";
const sourceMdPath = path.join(root, "docs", mdFileName);
fs.writeFileSync(sourceMdPath, mdContent, "utf8");
const rootUri = fileUrl(root);
// Loose Markdown 的 documentId 编码:local-md:{encoded_relative_path}
// `~2F` 是 URL 编码的 `/` 的替代分隔符(由 encode_local_id_segment 产生)
const documentId = "local-md:docs~2Ftest-page.md";
// trash 路径由 next_available_path 生成,文件名取 file_stem_title 即去掉 .md 扩展名
const trashFileName = "test-page.md";
const trashPath = path.join(root, ".mnote", "trash", trashFileName);
const trashIndexPath = path.join(root, ".mnote", "trash-index.json");
const steps = [];
try {
// ===== Step 1: delete -> archive to local trash =====
steps.push(await runStep("loose Markdown delete 进入本地回收站", async () => {
const result = await postTreeCommand({
action: "delete",
sourceKind: "local_folder",
rootUri,
documentId,
});
const exec = result.result?.execution || {};
// 当前 trash_local_markdown_page 不返回 canonicalCommand
// 资源 lifecycle 中非 md 文件会返回 "tree.resource.archive"
// 此处记录真实值,不强制断言不存在或特定值
const canonicalCommand = exec.canonicalCommand;
// 源文件消失
assert.equal(fs.existsSync(sourceMdPath), false, "Delete 后源 .md 文件应消失");
// trash 中出现对应文件
assert.equal(fs.existsSync(trashPath), true, "Delete 后文件应进入 .mnote/trash");
assert.equal(exec.resourceKind, "markdown", "delete 返回 resourceKind 应为 markdown");
// trash-index.json 记录 local-md:* entry
assert.ok(fs.existsSync(trashIndexPath), "trash-index.json 应存在");
const index = fs.readFileSync(trashIndexPath, "utf8");
assert.match(index, /local-md:docs~2Ftest-page\.md/, "trash index 应记录 local-md entry");
return { documentId, resourceKind: exec.resourceKind, canonicalCommand, trashIndexContains: true };
}));
// ===== Step 2: restore -> 恢复回原路径 =====
steps.push(await runStep("loose Markdown restore 回原路径", async () => {
const result = await postTreeCommand({
action: "restore",
sourceKind: "local_folder",
rootUri,
documentId,
});
const exec = result.result?.execution || {};
// 源 .md 恢复
assert.equal(fs.existsSync(sourceMdPath), true, "restore 后源 .md 文件应恢复");
assert.equal(fs.readFileSync(sourceMdPath, "utf8"), mdContent, "restore 后文件内容应不变");
// trash 文件消失
assert.equal(fs.existsSync(trashPath), false, "restore 后 trash 文件应消失");
// index 清理
const index = fs.readFileSync(trashIndexPath, "utf8");
assert.doesNotMatch(index, /local-md:docs~2Ftest-page\.md/, "restore 后 trash index 应清理 entry");
return { documentId: exec.documentId, previousDocumentId: exec.previousDocumentId };
}));
// ===== Step 3: 再次 delete =====
steps.push(await runStep("loose Markdown 再次 delete 进回收站", async () => {
const result = await postTreeCommand({
action: "delete",
sourceKind: "local_folder",
rootUri,
documentId,
});
const exec = result.result?.execution || {};
assert.equal(fs.existsSync(sourceMdPath), false, "再次 delete 后源 .md 文件应消失");
assert.equal(fs.existsSync(trashPath), true, "再次 delete 后 trash 文件应出现");
const index = fs.readFileSync(trashIndexPath, "utf8");
assert.match(index, /local-md:docs~2Ftest-page\.md/, "再次 delete 后 trash index 应恢复记录");
return { resourceKind: exec.resourceKind };
}));
// ===== Step 4: purge -> 永久删除 =====
steps.push(await runStep("loose Markdown purge 永久删除", async () => {
const result = await postTreeCommand({
action: "purge",
sourceKind: "local_folder",
rootUri,
documentId,
});
const exec = result.result?.execution || {};
// 源路径与 trash 路径均不存在
assert.equal(fs.existsSync(sourceMdPath), false, "purge 后源 .md 文件不应存在");
assert.equal(fs.existsSync(trashPath), false, "purge 后 trash 文件不应存在");
// index 清理
const index = fs.readFileSync(trashIndexPath, "utf8");
assert.doesNotMatch(index, /local-md:docs~2Ftest-page\.md/, "purge 后 trash index 应清理 entry");
return { ok: exec.ok };
}));
// ===== Step 5: 完整周期后再次创建+delete+purge,确保 trash-index.json 整体结构仍合法 =====
steps.push(await runStep("重新创建并清理验证 trash-index.json 完整性", async () => {
fs.writeFileSync(sourceMdPath, "# Second\n", "utf8");
await postTreeCommand({
action: "delete",
sourceKind: "local_folder",
rootUri,
documentId,
});
await postTreeCommand({
action: "purge",
sourceKind: "local_folder",
rootUri,
documentId,
});
// trash-index.json 应仍是合法 JSON
const indexRaw = fs.readFileSync(trashIndexPath, "utf8");
assert.doesNotThrow(() => JSON.parse(indexRaw), "trash-index.json 应为合法 JSON");
return { trashIndexValid: true };
}));
const result = {
ok: true,
task: "task475-local-folder-markdown-trash-lifecycle-smoke",
baseUrl: BASE_URL,
root,
documentId,
sourceType: "loose Markdown page (non-bundle)",
note: "loose Markdown 当前不返回 canonicalCommandtrash_local_markdown_page 无此字段),bundled Markdown case 未覆盖(bundle 涉及目录整体移动/恢复,Rust 单测覆盖更充分)",
steps,
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.log(`task475 local folder Markdown trash lifecycle smoke passed: ${RESULT_PATH}`);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
run().catch((error) => {
const result = {
ok: false,
task: "task475-local-folder-markdown-trash-lifecycle-smoke",
baseUrl: BASE_URL,
error: error && error.stack ? error.stack : String(error),
};
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});