/** * PluginStateRepository Tests * * Tests the database-backed plugin state storage for: * - CRUD operations (get, getAll, upsert, delete) * - Enable/disable convenience methods * - Timestamp tracking */ import Database from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runMigrations } from "../../../src/database/migrations/runner.js"; import type { Database as DbSchema } from "../../../src/database/types.js"; import { PluginStateRepository } from "../../../src/plugins/state.js"; describe("PluginStateRepository", () => { let db: Kysely; let sqliteDb: Database.Database; let repo: PluginStateRepository; beforeEach(async () => { // Create in-memory SQLite database sqliteDb = new Database(":memory:"); db = new Kysely({ dialect: new SqliteDialect({ database: sqliteDb, }), }); // Run migrations to create tables await runMigrations(db); repo = new PluginStateRepository(db); }); afterEach(async () => { await db.destroy(); sqliteDb.close(); }); describe("get", () => { it("returns null for non-existent plugin", async () => { const state = await repo.get("non-existent"); expect(state).toBeNull(); }); it("returns state for existing plugin", async () => { // Insert directly await db .insertInto("_plugin_state") .values({ plugin_id: "test-plugin", status: "active", version: "1.0.0", installed_at: new Date().toISOString(), activated_at: new Date().toISOString(), deactivated_at: null, data: null, }) .execute(); const state = await repo.get("test-plugin"); expect(state).not.toBeNull(); expect(state!.pluginId).toBe("test-plugin"); expect(state!.status).toBe("active"); expect(state!.version).toBe("1.0.0"); }); it("parses dates correctly", async () => { const now = new Date(); await db .insertInto("_plugin_state") .values({ plugin_id: "test-plugin", status: "inactive", version: "2.0.0", installed_at: now.toISOString(), activated_at: now.toISOString(), deactivated_at: now.toISOString(), data: null, }) .execute(); const state = await repo.get("test-plugin"); expect(state!.installedAt).toBeInstanceOf(Date); expect(state!.activatedAt).toBeInstanceOf(Date); expect(state!.deactivatedAt).toBeInstanceOf(Date); }); it("handles null dates", async () => { await db .insertInto("_plugin_state") .values({ plugin_id: "test-plugin", status: "inactive", version: "1.0.0", installed_at: new Date().toISOString(), activated_at: null, deactivated_at: null, data: null, }) .execute(); const state = await repo.get("test-plugin"); expect(state!.activatedAt).toBeNull(); expect(state!.deactivatedAt).toBeNull(); }); }); describe("getAll", () => { it("returns empty array when no plugins", async () => { const states = await repo.getAll(); expect(states).toEqual([]); }); it("returns all plugin states", async () => { await db .insertInto("_plugin_state") .values([ { plugin_id: "plugin-a", status: "active", version: "1.0.0", installed_at: new Date().toISOString(), activated_at: new Date().toISOString(), deactivated_at: null, data: null, }, { plugin_id: "plugin-b", status: "inactive", version: "2.0.0", installed_at: new Date().toISOString(), activated_at: null, deactivated_at: null, data: null, }, ]) .execute(); const states = await repo.getAll(); expect(states).toHaveLength(2); expect(states.map((s) => s.pluginId).toSorted()).toEqual(["plugin-a", "plugin-b"]); }); }); describe("upsert", () => { it("creates new state when plugin does not exist", async () => { const state = await repo.upsert("new-plugin", "1.0.0", "active"); expect(state.pluginId).toBe("new-plugin"); expect(state.version).toBe("1.0.0"); expect(state.status).toBe("active"); expect(state.installedAt).toBeInstanceOf(Date); }); it("updates existing state", async () => { // Create initial state await repo.upsert("test-plugin", "1.0.0", "active"); // Update it const state = await repo.upsert("test-plugin", "1.1.0", "inactive"); expect(state.pluginId).toBe("test-plugin"); expect(state.version).toBe("1.1.0"); expect(state.status).toBe("inactive"); }); it("sets activated_at when activating", async () => { // Create as inactive await repo.upsert("test-plugin", "1.0.0", "inactive"); // Activate const state = await repo.upsert("test-plugin", "1.0.0", "active"); expect(state.activatedAt).toBeInstanceOf(Date); }); it("sets deactivated_at when deactivating", async () => { // Create as active await repo.upsert("test-plugin", "1.0.0", "active"); // Deactivate const state = await repo.upsert("test-plugin", "1.0.0", "inactive"); expect(state.deactivatedAt).toBeInstanceOf(Date); }); it("does not change activated_at if already active", async () => { // Create as active const initial = await repo.upsert("test-plugin", "1.0.0", "active"); const initialActivatedAt = initial.activatedAt!.getTime(); // Wait a bit then update version (still active) await new Promise((r) => setTimeout(r, 10)); const updated = await repo.upsert("test-plugin", "1.1.0", "active"); // activated_at should be the same expect(updated.activatedAt!.getTime()).toBe(initialActivatedAt); }); }); describe("enable", () => { it("creates active state for new plugin", async () => { const state = await repo.enable("new-plugin", "1.0.0"); expect(state.status).toBe("active"); expect(state.activatedAt).toBeInstanceOf(Date); }); it("activates inactive plugin", async () => { await repo.upsert("test-plugin", "1.0.0", "inactive"); const state = await repo.enable("test-plugin", "1.0.0"); expect(state.status).toBe("active"); }); }); describe("disable", () => { it("creates inactive state for new plugin", async () => { const state = await repo.disable("new-plugin", "1.0.0"); expect(state.status).toBe("inactive"); expect(state.activatedAt).toBeNull(); }); it("deactivates active plugin", async () => { await repo.upsert("test-plugin", "1.0.0", "active"); const state = await repo.disable("test-plugin", "1.0.0"); expect(state.status).toBe("inactive"); expect(state.deactivatedAt).toBeInstanceOf(Date); }); }); describe("delete", () => { it("returns false for non-existent plugin", async () => { const deleted = await repo.delete("non-existent"); expect(deleted).toBe(false); }); it("deletes existing plugin and returns true", async () => { await repo.upsert("test-plugin", "1.0.0", "active"); const deleted = await repo.delete("test-plugin"); expect(deleted).toBe(true); // Verify it's gone const state = await repo.get("test-plugin"); expect(state).toBeNull(); }); it("only deletes specified plugin", async () => { await repo.upsert("plugin-a", "1.0.0", "active"); await repo.upsert("plugin-b", "1.0.0", "active"); await repo.delete("plugin-a"); const stateA = await repo.get("plugin-a"); const stateB = await repo.get("plugin-b"); expect(stateA).toBeNull(); expect(stateB).not.toBeNull(); }); }); });