import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { StateDb } from "./stateDb"; import * as sqliteAdapter from "./sqliteAdapter"; import { STATE_DB_SCHEMA_VERSION } from "./stateMigrations"; import * as stateMigrations from "./stateMigrations"; const tempDirs: string[] = []; function makeTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir; } afterEach(() => { for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); } vi.restoreAllMocks(); }); describe("StateDb", () => { it("creates the SQLite schema and applies migrations", () => { const tempDir = makeTempDir("t3code-state-db-"); const dbPath = path.join(tempDir, "state.sqlite"); const stateDb = new StateDb({ dbPath }); const tables = stateDb.db .prepare("SELECT name FROM sqlite_master WHERE type = 'table';") .all() as Array<{ name: string }>; const tableNames = new Set(tables.map((table) => table.name)); expect(tableNames.has("documents")).toBe(true); expect(tableNames.has("provider_events")).toBe(true); expect(tableNames.has("state_events")).toBe(true); expect(tableNames.has("metadata")).toBe(true); const userVersion = stateDb.db .prepare("PRAGMA user_version;") .get() as { user_version: number } | undefined; expect(userVersion?.user_version).toBe(STATE_DB_SCHEMA_VERSION); stateDb.close(); }); it("closes the database when migrations fail during construction", () => { const tempDir = makeTempDir("t3code-state-db-migration-fail-"); const dbPath = path.join(tempDir, "state.sqlite"); const close = vi.fn(); const fakeDb = { exec: vi.fn(), prepare: vi.fn(), close, }; vi.spyOn(sqliteAdapter, "openSqliteDatabase").mockReturnValue( fakeDb as unknown as sqliteAdapter.SqliteDatabase, ); vi.spyOn(stateMigrations, "runStateMigrations").mockImplementation(() => { throw new Error("migration failed"); }); expect(() => new StateDb({ dbPath })).toThrow("migration failed"); expect(close).toHaveBeenCalledTimes(1); }); it("preserves the original transaction error when rollback fails", () => { const tempDir = makeTempDir("t3code-state-db-rollback-fail-"); const dbPath = path.join(tempDir, "state.sqlite"); const rollbackError = new Error("rollback failed"); const exec = vi.fn((sql: string) => { if (sql === "ROLLBACK;") { throw rollbackError; } }); const fakeDb = { exec, prepare: vi.fn(), close: vi.fn(), }; vi.spyOn(sqliteAdapter, "openSqliteDatabase").mockReturnValue( fakeDb as unknown as sqliteAdapter.SqliteDatabase, ); vi.spyOn(stateMigrations, "runStateMigrations").mockImplementation(() => {}); const stateDb = new StateDb({ dbPath }); const originalError = new Error("operation failed"); let thrown: unknown; try { stateDb.transaction(() => { throw originalError; }); } catch (error) { thrown = error; } expect(thrown).toBe(originalError); expect(exec).toHaveBeenCalledWith("ROLLBACK;"); stateDb.close(); }); });