#!/usr/bin/env node "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 ACTOR_ID = "user_real"; function fileUrl(localPath) { return `file://${localPath}`; } function writeWorkspaceManifest(root) { fs.mkdirSync(path.join(root, ".mnote"), { recursive: true }); fs.writeFileSync( path.join(root, ".mnote", "workspace.json"), `${JSON.stringify({ workspaceId: "local-ws:user_real:task472", ownerId: ACTOR_ID, createdAt: new Date().toISOString(), capabilities: ["local_files", "tree_commands", "markdown_edit"], }, null, 2)}\n`, "utf8", ); } async function requestJson(requestPath, init = {}) { const response = await fetch(`${BASE_URL}${requestPath}`, { ...init, headers: { ...(init.body ? { "content-type": "application/json" } : {}), "x-mnote-actor-id": ACTOR_ID, "x-mnote-actor-type": "user", ...(init.headers || {}), }, }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { payload = text; } if (!response.ok) { throw new Error(`${requestPath} 请求失败: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`); } return payload; } async function readFileTreeProjection(rootUri) { const url = new URL(`${BASE_URL}/api/tree/projections/file`); url.searchParams.set("sourceKind", "local_folder"); url.searchParams.set("rootUri", rootUri); const payload = await requestJson(`${url.pathname}${url.search}`); const result = payload.result || payload; assert.equal(result.sourceKind, "local_folder", `projection 应来自 local_folder: ${JSON.stringify(result)}`); assert.equal(result.projection, "file_tree", `projection 类型应为 file_tree: ${JSON.stringify(result)}`); assert(Array.isArray(result.items), `projection items 应为数组: ${JSON.stringify(result)}`); return result; } function rootMarkdownOrder(projection) { return projection.items .filter((item) => item && item.parentNodeId == null && item.rowKind === "markdown") .map((item) => item.resourceMeta?.extra?.source?.relativePath || ""); } function readPersistedOrder(root) { const orderPath = path.join(root, ".mnote", "file-order.json"); assert(fs.existsSync(orderPath), "move sortOrder 后应写入 .mnote/file-order.json"); return JSON.parse(fs.readFileSync(orderPath, "utf8")); } async function moveWithSortOrder(rootUri, documentId, sortOrder) { const payload = await requestJson("/api/tree/commands", { method: "POST", body: JSON.stringify({ action: "move", sourceKind: "local_folder", rootUri, documentId, parentId: null, sortOrder, }), }); assert.equal(payload.result?.execution?._unsupportedFields, undefined, `sortOrder 不应再落入 _unsupportedFields: ${JSON.stringify(payload)}`); return payload; } async function main() { const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task472-sort-order-")); const rootUri = fileUrl(root); try { writeWorkspaceManifest(root); fs.writeFileSync(path.join(root, "alpha.md"), "# Alpha\n", "utf8"); fs.writeFileSync(path.join(root, "beta.md"), "# Beta\n", "utf8"); fs.writeFileSync(path.join(root, "source.md"), "# Source\n", "utf8"); const initialProjection = await readFileTreeProjection(rootUri); assert.deepEqual( rootMarkdownOrder(initialProjection).slice(0, 3), ["alpha.md", "beta.md", "source.md"], "初始 projection 应按文件系统自然顺序作为基线", ); await moveWithSortOrder(rootUri, "local-md:source.md", 0); const immediateProjection = await readFileTreeProjection(rootUri); assert.deepEqual( rootMarkdownOrder(immediateProjection).slice(0, 3), ["source.md", "alpha.md", "beta.md"], "move sortOrder 后立即重新加载 projection 应保持新顺序", ); const persistedOrder = readPersistedOrder(root); assert(JSON.stringify(persistedOrder).includes("source.md"), `file-order 应记录 source.md: ${JSON.stringify(persistedOrder)}`); const reloadedProjection = await readFileTreeProjection(rootUri); assert.deepEqual( rootMarkdownOrder(reloadedProjection).slice(0, 3), ["source.md", "alpha.md", "beta.md"], "再次重新加载 projection 顺序不应回退到自然排序", ); console.log(JSON.stringify({ ok: true, root, rootUri, order: rootMarkdownOrder(reloadedProjection).slice(0, 3), persistedOrder, }, null, 2)); } finally { if (!process.env.MNOTE_KEEP_SMOKE_TMP) { fs.rmSync(root, { recursive: true, force: true }); } } } main().catch((error) => { console.error(error); process.exit(1); });