#!/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 { spawnSync } = require("node:child_process"); const repoRoot = path.resolve(__dirname, "..", "..", ".."); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-convex-export-plan-")); const fixturePath = path.join(tmpRoot, "fixture.json"); const outRoot = path.join(tmpRoot, "local-workspace"); const manifestPath = path.join(tmpRoot, "migration-manifest.json"); const conflictPath = path.join(tmpRoot, "conflicts.json"); const fixture = { workspace: { id: "ws_legacy_plan", name: "迁移计划空间", ownerId: "user_plan", }, documents: [ { id: "doc_root", title: "Project", parent_id: null, content: "# Project\n\n导出正文\n", wide_layout: true, }, ], mediaAssets: [ { id: "asset_logo", fileName: "logo.txt", documentId: "doc_root", content: "LOGO-NEW", }, ], aiSessions: [], }; function runExport(args, expectedStatus = 0) { const result = spawnSync( process.execPath, [path.join(__dirname, "export-convex-workspace-to-local.js"), ...args], { cwd: repoRoot, encoding: "utf8" }, ); assert.equal(result.status, expectedStatus, result.stderr || result.stdout); return result; } try { fs.writeFileSync(fixturePath, JSON.stringify(fixture, null, 2), "utf8"); runExport([ "--fixture", fixturePath, "--out", outRoot, "--dry-run", "--manifest", manifestPath, "--conflict-report", conflictPath, ]); assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.md")), false, "dry run 不应写入页面文件"); const dryRunManifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); assert.equal(dryRunManifest.dryRun, true); assert.equal(dryRunManifest.operations.some((item) => item.action === "create" && item.relativePath === "pages/Project.md"), true); fs.mkdirSync(path.join(outRoot, "pages"), { recursive: true }); fs.writeFileSync(path.join(outRoot, "pages", "Project.md"), "原有内容\n", "utf8"); const conflict = runExport([ "--fixture", fixturePath, "--out", outRoot, "--manifest", manifestPath, "--conflict-report", conflictPath, ], 2); assert.match(conflict.stderr + conflict.stdout, /migration_conflict/); assert.equal(fs.readFileSync(path.join(outRoot, "pages", "Project.md"), "utf8"), "原有内容\n"); const conflictReport = JSON.parse(fs.readFileSync(conflictPath, "utf8")); assert.equal(conflictReport.conflicts.some((item) => item.relativePath === "pages/Project.md"), true); fs.rmSync(outRoot, { recursive: true, force: true }); runExport([ "--fixture", fixturePath, "--out", outRoot, "--manifest", manifestPath, ]); assert.match(fs.readFileSync(path.join(outRoot, "pages", "Project.md"), "utf8"), /导出正文/); assert.equal(fs.existsSync(path.join(outRoot, ".mnote", "migration-manifest.json")), true); runExport(["--rollback", path.join(outRoot, ".mnote", "migration-manifest.json")]); assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.md")), false, "rollback 应删除本次新增页面"); assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.assets", "logo.txt")), false, "rollback 应删除本次新增资源"); console.log(JSON.stringify({ ok: true, smoke: "task455-convex-export-plan-rollback" }, null, 2)); } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); }