import { Database } from "bun:sqlite"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { clearLocalMappings } from "./clear-local-mappings"; let sqlite: Database; let tempDir: string; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "scribe-clear-mappings-")); sqlite = new Database(join(tempDir, "scribe.db"), { create: true }); sqlite.exec(` CREATE TABLE projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE project_roots ( id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL ); CREATE TABLE invoice_line_items ( id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL ); `); }); afterEach(() => { sqlite.close(); rmSync(tempDir, { recursive: true, force: true }); }); function insertProject(id: string, name: string, rootIds: string[]): void { sqlite.prepare("INSERT INTO projects (id, name) VALUES (?, ?)").run(id, name); const insertRoot = sqlite.prepare( "INSERT INTO project_roots (id, project_id) VALUES (?, ?)", ); for (const rootId of rootIds) insertRoot.run(rootId, id); } function mappedRoots(): Array<{ id: string; projectId: string | null }> { return sqlite .query<{ id: string; projectId: string | null }, []>( "SELECT id, project_id AS projectId FROM project_roots ORDER BY id", ) .all(); } function projectIds(): string[] { return sqlite .query<{ id: string }, []>("SELECT id FROM projects ORDER BY id") .all() .map((row) => row.id); } describe("clearLocalMappings", () => { test("all unmaps every root, removes emptied groups, and keeps invoice-linked groups", () => { insertProject("auto-a", "Auto A", ["root-a", "root-b"]); insertProject("invoiced", "Invoiced", ["root-invoice"]); sqlite .prepare( "INSERT INTO invoice_line_items (id, project_id) VALUES ('line-1', 'invoiced')", ) .run(); expect(clearLocalMappings(sqlite, { scope: "all" })).toEqual({ unmappedRoots: 3, deletedGroups: 1, protectedInvoiceGroups: 1, }); expect(mappedRoots()).toEqual([ { id: "root-a", projectId: null }, { id: "root-b", projectId: null }, { id: "root-invoice", projectId: null }, ]); expect(projectIds()).toEqual(["invoiced"]); }); test("non-cloud preserves matching groups and clears the rest", () => { insertProject("cloud", " Cloud Project ", ["root-cloud"]); insertProject("local", "Local Project", ["root-local"]); expect( clearLocalMappings(sqlite, { scope: "non-cloud", cloudGroupKeys: new Set(["cloud project"]), }), ).toEqual({ unmappedRoots: 1, deletedGroups: 1, protectedInvoiceGroups: 0, }); expect(mappedRoots()).toEqual([ { id: "root-cloud", projectId: "cloud" }, { id: "root-local", projectId: null }, ]); expect(projectIds()).toEqual(["cloud"]); }); test("is idempotent", () => { insertProject("auto", "Auto", ["root"]); expect(clearLocalMappings(sqlite, { scope: "all" })).toEqual({ unmappedRoots: 1, deletedGroups: 1, protectedInvoiceGroups: 0, }); expect(clearLocalMappings(sqlite, { scope: "all" })).toEqual({ unmappedRoots: 0, deletedGroups: 0, protectedInvoiceGroups: 0, }); }); });