From e73aeb4aaf406d481161ffd8487cf20b33164cad Mon Sep 17 00:00:00 2001 From: Hibryda Date: Sun, 22 Mar 2026 04:58:37 +0100 Subject: [PATCH] test(electrobun): settings-db unit tests (partial, agents still running) --- .../src/bun/__tests__/settings-db.test.ts | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 ui-electrobun/src/bun/__tests__/settings-db.test.ts diff --git a/ui-electrobun/src/bun/__tests__/settings-db.test.ts b/ui-electrobun/src/bun/__tests__/settings-db.test.ts new file mode 100644 index 0000000..ce848e4 --- /dev/null +++ b/ui-electrobun/src/bun/__tests__/settings-db.test.ts @@ -0,0 +1,235 @@ +/** + * Unit tests for SettingsDb — in-memory SQLite, no filesystem side effects. + * We cannot import SettingsDb directly (singleton hits filesystem), + * so we replicate the schema on an in-memory Database and exercise the SQL logic. + */ +import { describe, it, expect, beforeEach } from "bun:test"; +import { Database } from "bun:sqlite"; + +// ── Schema (mirrors settings-db.ts) ───────────────────────────────────────── + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL); +CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS projects (id TEXT PRIMARY KEY, config TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS custom_themes ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, palette TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS groups ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT NOT NULL, position INTEGER NOT NULL +); +CREATE TABLE IF NOT EXISTS keybindings (id TEXT PRIMARY KEY, chord TEXT NOT NULL); +`; + +const SEED_GROUPS = ` +INSERT OR IGNORE INTO groups VALUES ('dev', 'Development', 'wrench', 0); +INSERT OR IGNORE INTO groups VALUES ('test', 'Testing', 'flask', 1); +INSERT OR IGNORE INTO groups VALUES ('ops', 'DevOps', 'rocket', 2); +INSERT OR IGNORE INTO groups VALUES ('research', 'Research', 'scope', 3); +`; + +function createDb(): Database { + const db = new Database(":memory:"); + db.exec(SCHEMA); + db.exec(SEED_GROUPS); + return db; +} + +// ── Helpers mirroring SettingsDb methods ───────────────────────────────────── + +function getSetting(db: Database, key: string): string | null { + const row = db.query<{ value: string }, [string]>( + "SELECT value FROM settings WHERE key = ?" + ).get(key); + return row?.value ?? null; +} + +function setSetting(db: Database, key: string, value: string): void { + db.query( + "INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value" + ).run(key, value); +} + +function getAll(db: Database): Record { + const rows = db.query<{ key: string; value: string }, []>( + "SELECT key, value FROM settings" + ).all(); + return Object.fromEntries(rows.map((r) => [r.key, r.value])); +} + +// ── Tests ─────────────────────────────────────────────────────────────────── + +describe("SettingsDb", () => { + let db: Database; + + beforeEach(() => { + db = createDb(); + }); + + // ── Settings CRUD ─────────────────────────────────────────────────────── + + describe("getSetting / setSetting", () => { + it("returns null for missing key", () => { + expect(getSetting(db, "nonexistent")).toBeNull(); + }); + + it("stores and retrieves a setting", () => { + setSetting(db, "theme", "mocha"); + expect(getSetting(db, "theme")).toBe("mocha"); + }); + + it("upserts on conflict", () => { + setSetting(db, "theme", "mocha"); + setSetting(db, "theme", "latte"); + expect(getSetting(db, "theme")).toBe("latte"); + }); + }); + + describe("getAll", () => { + it("returns empty object when no settings exist", () => { + expect(getAll(db)).toEqual({}); + }); + + it("returns all key-value pairs", () => { + setSetting(db, "a", "1"); + setSetting(db, "b", "2"); + expect(getAll(db)).toEqual({ a: "1", b: "2" }); + }); + }); + + // ── Projects CRUD ────────────────────────────────────────────────────── + + describe("projects", () => { + it("setProject + getProject round-trip", () => { + const config = { id: "p1", name: "Test", cwd: "/tmp" }; + db.query( + "INSERT INTO projects (id, config) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET config = excluded.config" + ).run("p1", JSON.stringify(config)); + + const row = db.query<{ config: string }, [string]>( + "SELECT config FROM projects WHERE id = ?" + ).get("p1"); + expect(JSON.parse(row!.config)).toEqual(config); + }); + + it("listProjects returns all", () => { + db.query("INSERT INTO projects VALUES (?, ?)").run("a", JSON.stringify({ id: "a" })); + db.query("INSERT INTO projects VALUES (?, ?)").run("b", JSON.stringify({ id: "b" })); + const rows = db.query<{ config: string }, []>("SELECT config FROM projects").all(); + expect(rows).toHaveLength(2); + }); + + it("deleteProject removes entry", () => { + db.query("INSERT INTO projects VALUES (?, ?)").run("x", JSON.stringify({ id: "x" })); + db.query("DELETE FROM projects WHERE id = ?").run("x"); + const row = db.query<{ config: string }, [string]>( + "SELECT config FROM projects WHERE id = ?" + ).get("x"); + expect(row).toBeNull(); + }); + }); + + // ── Groups CRUD ──────────────────────────────────────────────────────── + + describe("groups", () => { + it("seeds 4 default groups", () => { + const rows = db.query<{ id: string }, []>( + "SELECT id FROM groups ORDER BY position" + ).all(); + expect(rows.map((r) => r.id)).toEqual(["dev", "test", "ops", "research"]); + }); + + it("createGroup upserts", () => { + db.query( + "INSERT INTO groups (id, name, icon, position) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name" + ).run("custom", "Custom", "star", 10); + const row = db.query<{ name: string }, [string]>( + "SELECT name FROM groups WHERE id = ?" + ).get("custom"); + expect(row!.name).toBe("Custom"); + }); + + it("deleteGroup removes entry", () => { + db.query("DELETE FROM groups WHERE id = ?").run("ops"); + const rows = db.query<{ id: string }, []>("SELECT id FROM groups").all(); + expect(rows.map((r) => r.id)).not.toContain("ops"); + }); + }); + + // ── Custom Themes CRUD ───────────────────────────────────────────────── + + describe("custom themes", () => { + it("save and retrieve theme", () => { + const palette = { base: "#1e1e2e", text: "#cdd6f4" }; + db.query( + "INSERT INTO custom_themes (id, name, palette) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET palette = excluded.palette" + ).run("my-theme", "My Theme", JSON.stringify(palette)); + + const row = db.query<{ palette: string }, [string]>( + "SELECT palette FROM custom_themes WHERE id = ?" + ).get("my-theme"); + expect(JSON.parse(row!.palette)).toEqual(palette); + }); + + it("deleteCustomTheme removes entry", () => { + db.query("INSERT INTO custom_themes VALUES (?, ?, ?)").run("t1", "T1", "{}"); + db.query("DELETE FROM custom_themes WHERE id = ?").run("t1"); + const row = db.query<{ id: string }, [string]>( + "SELECT id FROM custom_themes WHERE id = ?" + ).get("t1"); + expect(row).toBeNull(); + }); + }); + + // ── Keybindings CRUD ─────────────────────────────────────────────────── + + describe("keybindings", () => { + it("set and retrieve keybinding", () => { + db.query( + "INSERT INTO keybindings (id, chord) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET chord = excluded.chord" + ).run("toggle-sidebar", "Ctrl+B"); + + const rows = db.query<{ id: string; chord: string }, []>( + "SELECT id, chord FROM keybindings" + ).all(); + expect(Object.fromEntries(rows.map((r) => [r.id, r.chord]))).toEqual({ + "toggle-sidebar": "Ctrl+B", + }); + }); + + it("deleteKeybinding removes entry", () => { + db.query("INSERT INTO keybindings VALUES (?, ?)").run("k1", "Ctrl+K"); + db.query("DELETE FROM keybindings WHERE id = ?").run("k1"); + const row = db.query<{ id: string }, [string]>( + "SELECT id FROM keybindings WHERE id = ?" + ).get("k1"); + expect(row).toBeNull(); + }); + }); + + // ── Schema version tracking ──────────────────────────────────────────── + + describe("schema_version", () => { + it("starts empty and can be seeded", () => { + const row = db.query<{ version: number }, []>( + "SELECT version FROM schema_version LIMIT 1" + ).get(); + expect(row).toBeNull(); // not yet inserted + + db.exec("INSERT INTO schema_version (version) VALUES (1)"); + const after = db.query<{ version: number }, []>( + "SELECT version FROM schema_version LIMIT 1" + ).get(); + expect(after!.version).toBe(1); + }); + + it("updates version", () => { + db.exec("INSERT INTO schema_version (version) VALUES (1)"); + db.exec("UPDATE schema_version SET version = 2"); + const row = db.query<{ version: number }, []>( + "SELECT version FROM schema_version LIMIT 1" + ).get(); + expect(row!.version).toBe(2); + }); + }); +});