Files
Termix/plugins/ai/tests/backend/migrations.test.ts
T

119 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
import { LEGACY_DDL, pluginDir } from "./helpers";
let db: TestDb | null = null;
afterEach(() => {
db?.close();
db = null;
});
const tables = (sqlite: TestDb["sqlite"]) =>
(
sqlite
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '%ai_%' ORDER BY name",
)
.all() as Array<{ name: string }>
).map((row) => row.name);
const indexes = (sqlite: TestDb["sqlite"], table: string) =>
(
sqlite
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name LIKE 'idx_%' ORDER BY name",
)
.all(table) as Array<{ name: string }>
).map((row) => row.name);
describe("ai adoption migration", () => {
it("renames the legacy tables and keeps every row, index and cascade", async () => {
db = await createTestDb(pluginDir, {
before: (sqlite) => {
sqlite.exec(LEGACY_DDL);
sqlite
.prepare("INSERT INTO users (id, username) VALUES ('u1', 'u1')")
.run();
sqlite.exec(`
INSERT INTO ai_providers (id, user_id, provider_type, label, api_key, api_key_prefix)
VALUES (3, 'u1', 'ollama', 'Local', 'enc-key', 'abc123');
INSERT INTO ai_conversations (id, user_id, title) VALUES (5, 'u1', 'Disk');
INSERT INTO ai_messages (conversation_id, role, content) VALUES (5, 'user', 'why full');
INSERT INTO ai_proposals (conversation_id, user_id, kind) VALUES (5, 'u1', 'propose_run_command');
`);
},
});
expect(tables(db.sqlite)).toEqual([
"p_ai_conversations",
"p_ai_messages",
"p_ai_proposals",
"p_ai_providers",
]);
const provider = db.sqlite
.prepare("SELECT * FROM p_ai_providers WHERE id = 3")
.get() as Record<string, unknown>;
// The key stays until core's boot migration moves it into ctx.secrets.
expect(provider).toMatchObject({ label: "Local", api_key: "enc-key" });
expect(indexes(db.sqlite, "p_ai_providers")).toEqual([
"idx_ai_providers_user_label",
]);
expect(indexes(db.sqlite, "p_ai_proposals")).toEqual([
"idx_ai_proposals_conversation",
"idx_ai_proposals_user",
]);
// The rename carried the foreign keys: a conversation still takes its
// messages and proposals with it.
db.sqlite.prepare("DELETE FROM p_ai_conversations WHERE id = 5").run();
const count = (table: string) =>
(
db!.sqlite.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
n: number;
}
).n;
expect(count("p_ai_messages")).toBe(0);
expect(count("p_ai_proposals")).toBe(0);
});
it("creates fresh tables when there is nothing to adopt", async () => {
db = await createTestDb(pluginDir);
expect(tables(db.sqlite)).toEqual([
"p_ai_conversations",
"p_ai_messages",
"p_ai_proposals",
"p_ai_providers",
]);
const columns = (
db.sqlite.prepare("PRAGMA table_info(p_ai_providers)").all() as Array<{
name: string;
}>
).map((column) => column.name);
expect(columns).not.toContain("api_key");
db.sqlite
.prepare("INSERT INTO users (id, username) VALUES ('u1', 'u1')")
.run();
db.sqlite
.prepare("INSERT INTO p_ai_conversations (id, user_id) VALUES (1, 'u1')")
.run();
db.sqlite
.prepare(
"INSERT INTO p_ai_messages (conversation_id, role) VALUES (1, 'user')",
)
.run();
db.sqlite.prepare("DELETE FROM p_ai_conversations WHERE id = 1").run();
expect(
(
db.sqlite.prepare("SELECT COUNT(*) AS n FROM p_ai_messages").get() as {
n: number;
}
).n,
).toBe(0);
});
});