Files
mnote/scripts/task-control-plane-admin-libsql-roundtrip-smoke.js
T

201 lines
7.1 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
"use strict";
/**
* task-control-plane-admin-libsql-roundtrip-smoke.js
*
* 验证 control-plane-admin CLI 的 SQLite → libSQL local → SQLite 往返:
* init → migrate-sqlite-to-target → export-target-to-sqlite
*
* 假设:
* - 工作区根 /mnt/Data1T/mnote
* - cargo 工作区包含 control-plane 包
* - libsql (turso) 原生依赖已可运行 (cargo build 通过)
*
* 不使用:
* - 生产 control-plane 路径
* - sqlite3 CLI
* 所有数据库文件创建在 /tmp
*/
const assert = require("node:assert");
const { spawnSync } = require("node:child_process");
const fs = require("node:fs/promises");
const path = require("node:path");
// ─── 配置 ──────────────────────────────────────────────────────────
const PROJECT_ROOT = "/mnt/Data1T/mnote";
const CARGO_MANIFEST = path.join(PROJECT_ROOT, "rust/Cargo.toml");
const PKG = "control-plane";
const BIN = "control-plane-admin";
const TMP_PREFIX = path.join("/tmp", `cp-admin-smoke-${Date.now().toString(36)}`);
// ─── 检查的键表 ────────────────────────────────────────────────────
const KEY_TABLES = ["users", "workspaces", "directory_grants", "auth_identities"];
// ─── 辅助 ──────────────────────────────────────────────────────────
function cargoRun(...args) {
const fullArgs = [
"run",
"--manifest-path", CARGO_MANIFEST,
"-p", PKG,
"--bin", BIN,
"--",
...args,
];
const result = spawnSync("cargo", fullArgs, {
cwd: PROJECT_ROOT,
encoding: "utf-8",
maxBuffer: 16 * 1024 * 1024,
env: { ...process.env, RUST_LOG: "error" },
});
const stdout = (result.stdout || "").trim();
const stderr = (result.stderr || "").trim();
if (result.error) {
throw new Error(`cargo run 执行失败: ${result.error.message}\nARGS: ${fullArgs.join(" ")}`);
}
return { code: result.status, stdout, stderr };
}
function parseJsonOutput(text, label) {
try {
return JSON.parse(text);
} catch (e) {
throw new Error(`${label}: 无法解析命令 JSON 输出\n原始输出:\n${text}`);
}
}
function assertOk(json, label) {
assert.strictEqual(
json.ok, true,
`${label}: json.ok 应为 true,实际为 ${JSON.stringify(json.ok)}`,
);
}
function assertTableRows(json, label) {
const tables = json.tables;
assert(tables !== undefined && tables !== null, `${label}: 应包含 tables 字段`);
const missing = KEY_TABLES.filter((t) => !(t in tables));
assert.strictEqual(
missing.length, 0,
`${label}: tables 缺少键表: ${missing.join(", ")}`,
);
for (const table of KEY_TABLES) {
const count = tables[table];
assert(
typeof count === "number" && count > 0,
`${label}: ${table} 行数应为正值,实际为 ${count}`,
);
}
}
async function cleanTempFiles() {
try {
const dir = path.dirname(TMP_PREFIX);
const prefix = path.basename(TMP_PREFIX);
const entries = await fs.readdir(dir);
for (const entry of entries) {
if (entry.startsWith(prefix)) {
await fs.rm(path.join(dir, entry), { force: true, recursive: true });
}
}
} catch { /* cleanup 失败不阻塞最终报告 */ }
}
// ─── 主流程 ────────────────────────────────────────────────────────
async function main() {
const sourceDb = `${TMP_PREFIX}-source.db`;
const targetDb = `${TMP_PREFIX}-target.db`;
const exportDb = `${TMP_PREFIX}-export.db`;
let sourceOk = false;
let migrateOk = false;
let exportOk = false;
console.log("=== control-plane-admin libSQL 往返 smoke ===");
console.log(` source: ${sourceDb}`);
console.log(` target: ${targetDb}`);
console.log(` export: ${exportDb}\n`);
try {
// ─── Step 1: init ─────────────────────────────────────────────
console.log("── Step 1: init (SQLite) ──");
const r1 = cargoRun(
"init",
"--backend", "sqlite",
"--sqlite-path", sourceDb,
);
assert.strictEqual(r1.code, 0, `init exit code = ${r1.code}:\n${r1.stderr}`);
const j1 = parseJsonOutput(r1.stdout, "init");
assertOk(j1, "init");
console.log(` ok, userId=${j1.userId}, workspaceId=${j1.workspaceId}\n`);
sourceOk = true;
// ─── Step 2: migrate-sqlite-to-target ─────────────────────────
console.log("── Step 2: migrate-sqlite-to-target (→libSQL local) ──");
const r2 = cargoRun(
"migrate-sqlite-to-target",
"--source", sourceDb,
"--backend", "libsql-local",
"--libsql-path", targetDb,
"--reset-target",
);
assert.strictEqual(r2.code, 0, `migrate exit code = ${r2.code}:\n${r2.stderr}`);
const j2 = parseJsonOutput(r2.stdout, "migrate-sqlite-to-target");
assertOk(j2, "migrate-sqlite-to-target");
assertTableRows(j2, "migrate-sqlite-to-target");
console.log(` ok, tables=${JSON.stringify(j2.tables)}\n`);
migrateOk = true;
// ─── Step 3: export-target-to-sqlite ──────────────────────────
console.log("── Step 3: export-target-to-sqlite (libSQL→SQLite) ──");
const r3 = cargoRun(
"export-target-to-sqlite",
"--backend", "libsql-local",
"--libsql-path", targetDb,
"--output", exportDb,
"--backup-existing",
);
assert.strictEqual(r3.code, 0, `export exit code = ${r3.code}:\n${r3.stderr}`);
const j3 = parseJsonOutput(r3.stdout, "export-target-to-sqlite");
assertOk(j3, "export-target-to-sqlite");
assertTableRows(j3, "export-target-to-sqlite");
console.log(` ok, tables=${JSON.stringify(j3.tables)}\n`);
exportOk = true;
// ─── 汇总 ─────────────────────────────────────────────────────
console.log("=== 全部通过 ===");
console.log(` init: PASS (userId=${j1.userId})`);
console.log(` migrate: PASS (tables=${Object.keys(j2.tables).length})`);
console.log(` export: PASS (tables=${Object.keys(j3.tables).length})`);
// 确认关键表在两次转储中行数一致
for (const table of KEY_TABLES) {
const srcCount = j2.tables[table];
const expCount = j3.tables[table];
assert.strictEqual(
srcCount, expCount,
`${table} 行数不一致: migrate=${srcCount}, export=${expCount}`,
);
}
console.log(" 行数一致性检查: PASS");
} catch (err) {
console.error("\n❌ FAILED:", err.message);
process.exitCode = 1;
} finally {
await cleanTempFiles();
console.log("\n清理完成");
}
}
main().catch((err) => {
console.error("unhandled error:", err);
process.exitCode = 1;
});