feat: 持久化从 JSON 迁移到 DuckDB,启动自动迁移存量数据并全面异步化
build-and-deploy / build-and-deploy (push) Successful in 37s

- 新增 src/persistence/duckdbAdapter:与 jsonAdapter 同接口的异步适配器,
  数据存于 data/registry.duckdb,首次启动自动从 JSON 迁移并备份 json-backup-<ts>
- 全部服务层(project/config/user/token)与路由改为 async/await
- MCP 工具与鉴权同步异步化,系统 mode 读写迁移至 DuckDB
- 服务启动流程:init DB -> 自动迁移 -> 确保默认管理员 -> listen
- 新增 DuckDB 往返/迁移测试,测试目录隔离 REGISTRY_DATA_DIR
This commit is contained in:
jif2.zhang
2026-09-14 10:35:38 +08:00
parent 2ba09fa7a1
commit 6bbd4bc471
19 changed files with 958 additions and 302 deletions
+97
View File
@@ -0,0 +1,97 @@
/**
* DuckDB 适配器单元测试
* 覆盖:写入/读取往返、JSON 异构值保留、JSON 存量自动迁移、重复初始化幂等
* 运行:npm test
*/
const test = require("node:test");
const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
// 使用临时数据目录,避免污染真实 data/
const tmpDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "registry-duckdb-test-"));
process.env.REGISTRY_DATA_DIR = tmpDataDir;
const {
init,
loadProjects,
saveProjects,
loadConfigs,
saveConfigs,
deleteConfigs,
loadSystem,
saveSystem,
loadMcpConfig,
saveMcpConfig,
loadUsers,
saveUsers,
loadApiTokens,
saveApiTokens,
} = require("../src/persistence/duckdbAdapter");
test("projects 写入/读取往返,字段无损保留", async () => {
await init();
const projects = [
{ id: "p1", name: "项目A", enableIdc: true, nested: { arr: [1, 2, 3] } },
{ id: "p2", name: "项目B", enableEnvironment: false },
];
await saveProjects(projects);
const loaded = await loadProjects();
assert.strictEqual(loaded.length, 2);
assert.deepStrictEqual(loaded.find((p) => p.id === "p1").nested, { arr: [1, 2, 3] });
});
test("configs 写入/读取往返,值类型异构保留", async () => {
await init();
const configs = [
{ id: "c1", key: "count", value: 42, type: "number" },
{ id: "c2", key: "flag", value: false, type: "boolean" },
{ id: "c3", key: "raw", value: { deep: ["a", null, true] }, type: "json" },
];
await saveConfigs("proj-x", configs);
const loaded = await loadConfigs("proj-x");
assert.strictEqual(loaded.length, 3);
assert.strictEqual(loaded.find((c) => c.id === "c1").value, 42);
assert.strictEqual(loaded.find((c) => c.id === "c2").value, false);
assert.deepStrictEqual(loaded.find((c) => c.id === "c3").value, { deep: ["a", null, true] });
// ensure projectId 未被注入进记录本体
assert.strictEqual("projectId" in loaded[0], false);
// 其他项目不受影响
assert.deepStrictEqual(await loadConfigs("proj-other"), []);
});
test("configs 删除仅作用于指定项目", async () => {
await init();
await saveConfigs("proj-keep", [{ id: "k1", key: "a", value: 1 }]);
await deleteConfigs("proj-keep");
await saveConfigs("proj-keep2", [{ id: "k2", key: "b", value: 2 }]);
await deleteConfigs("proj-keep2");
await saveConfigs("proj-keep3", [{ id: "k3", key: "c", value: 3 }]);
await deleteConfigs("proj-keep3");
await saveConfigs("proj-del", [{ id: "d1", key: "x", value: 1 }]);
await deleteConfigs("proj-del");
await deleteConfigs("proj-del");
assert.deepStrictEqual(await loadConfigs("proj-del"), []);
});
test("system / mcp 写入读取与默认值", async () => {
await init();
const sys = await loadSystem();
assert.strictEqual(sys.mode, "edit");
await saveSystem({ mode: "readonly" });
assert.strictEqual((await loadSystem()).mode, "readonly");
const mcp = await loadMcpConfig();
assert.strictEqual(typeof mcp.token, "string");
await saveMcpConfig({ enabled: false, token: "abc" });
assert.deepStrictEqual(await loadMcpConfig(), { enabled: false, token: "abc" });
});
test("users / apiTokens 写入读取往返", async () => {
await init();
await saveUsers([{ id: "u1", username: "admin", password: "hashed", roles: ["admin"] }]);
const users = await loadUsers();
assert.strictEqual(users[0].username, "admin");
await saveApiTokens([{ id: "t1", name: "mcp", value: "secret" }]);
assert.strictEqual((await loadApiTokens())[0].value, "secret");
});