feat: add the feature for user save combo

This commit is contained in:
2026-07-16 16:52:37 +07:00
parent e3cf2e7708
commit 448a8aedc7
20 changed files with 1215 additions and 114 deletions
+104
View File
@@ -0,0 +1,104 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const originalDataDir = process.env.DATA_DIR;
let tempDir;
let db;
let ownerOne;
let ownerTwo;
beforeAll(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-cli-tool-config-"));
process.env.DATA_DIR = tempDir;
try { global._dbAdapter?.instance?.close?.(); } catch {}
delete global._dbAdapter;
vi.resetModules();
db = await import("@/lib/db/index.js");
await db.initDb();
ownerOne = await db.createUser({ username: "cli-owner-one", password: "password", role: "admin" });
ownerTwo = await db.createUser({ username: "cli-owner-two", password: "password", role: "user" });
});
afterAll(() => {
try { global._dbAdapter?.instance?.close?.(); } catch {}
delete global._dbAdapter;
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
const claudeConfig = (model) => ({
baseUrl: "https://router.example/v1",
apiKeyMode: "managed",
apiKeyId: null,
claudeModels: { sonnet: model, opus: "", haiku: "" },
claudeThinking: { sonnet: "high" },
});
describe("CLI tool configuration persistence", () => {
it("round-trips and atomically overwrites one owner/tool row", async () => {
const created = await db.upsertCliToolConfig(ownerOne.id, "claude", claudeConfig("cc/sonnet-a"));
expect(created).toMatchObject({ ownerId: ownerOne.id, toolId: "claude", config: claudeConfig("cc/sonnet-a") });
const updated = await db.upsertCliToolConfig(ownerOne.id, "claude", claudeConfig("cc/sonnet-b"));
expect(updated.config.claudeModels.sonnet).toBe("cc/sonnet-b");
expect((await db.getCliToolConfigsByOwnerId(ownerOne.id)).filter((row) => row.toolId === "claude")).toHaveLength(1);
});
it("isolates users and permits concurrent saves to different tools", async () => {
await Promise.all([
db.upsertCliToolConfig(ownerOne.id, "cursor", {
apiKeyMode: "custom",
selectedModels: ["cc/a", "cx/b"],
}),
db.upsertCliToolConfig(ownerTwo.id, "cursor", {
apiKeyMode: "managed",
apiKeyId: null,
selectedModels: ["gg/c"],
}),
db.upsertCliToolConfig(ownerOne.id, "codex", {
baseUrl: "https://router.example/v1",
apiKeyMode: "managed",
apiKeyId: null,
codexModel: "cx/gpt",
codexThinking: "high",
}),
]);
expect((await db.getCliToolConfig(ownerOne.id, "cursor")).config.selectedModels).toEqual(["cc/a", "cx/b"]);
expect((await db.getCliToolConfig(ownerTwo.id, "cursor")).config.selectedModels).toEqual(["gg/c"]);
expect((await db.getCliToolConfig(ownerOne.id, "codex")).config.codexModel).toBe("cx/gpt");
});
it("includes valid configurations in export/import and skips malformed rows", async () => {
const exported = await db.exportDb();
expect(exported.cliToolConfigs).toEqual(expect.arrayContaining([
expect.objectContaining({ ownerId: ownerOne.id, toolId: "claude" }),
expect.objectContaining({ ownerId: ownerTwo.id, toolId: "cursor" }),
]));
exported.cliToolConfigs.push({
ownerId: ownerOne.id,
toolId: "kiro",
config: { apiKey: "must-not-import" },
});
await db.importDb(exported);
expect(await db.getCliToolConfig(ownerOne.id, "claude")).not.toBeNull();
expect(await db.getCliToolConfig(ownerOne.id, "kiro")).toBeNull();
});
it("removes configurations when their owner is deleted", async () => {
const disposable = await db.createUser({ username: "cli-disposable", password: "password", role: "user" });
await db.upsertCliToolConfig(disposable.id, "cursor", {
apiKeyMode: "managed",
apiKeyId: null,
selectedModels: ["cc/a"],
});
expect(await db.deleteUser(disposable.id)).toBe(true);
expect(await db.getCliToolConfig(disposable.id, "cursor")).toBeNull();
});
});