import { Database } from "bun:sqlite"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { deduplicateProjectRootsByPath } from "../project-root-dedup"; let sqlite: Database; beforeEach(() => { sqlite = new Database(":memory:"); sqlite.exec(` PRAGMA foreign_keys = ON; CREATE TABLE project_roots ( id TEXT PRIMARY KEY, scan_root_id TEXT, project_id TEXT, device_id TEXT, path TEXT NOT NULL, detected_name TEXT, git_remote TEXT, peacock_color TEXT, created_at INTEGER ); CREATE TABLE project_associations ( id TEXT PRIMARY KEY, project_root_id TEXT REFERENCES project_roots(id) ON DELETE CASCADE, pattern TEXT NOT NULL ); CREATE TABLE timeline_events ( id TEXT PRIMARY KEY, project_root_id TEXT REFERENCES project_roots(id) ON DELETE SET NULL ); CREATE TABLE live_activity_sessions ( id TEXT PRIMARY KEY, project_root_id TEXT REFERENCES project_roots(id) ON DELETE SET NULL ); `); }); afterEach(() => { sqlite.close(); }); describe("deduplicateProjectRootsByPath", () => { test("merges a remote/null pair and re-points every root reference", () => { const insertRoot = sqlite.prepare( `INSERT INTO project_roots (id, scan_root_id, project_id, device_id, path, detected_name, git_remote, peacock_color, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ); insertRoot.run( "remote-root", "git-scan", null, null, "/Users/test/code/a2b", "a2b", "https://github.com/b-open-io/a2b.git", null, 2, ); insertRoot.run( "assigned-root", "files-scan", "project-a2b", null, "/Users/test/code/a2b", "a2b", null, "blue", 1, ); sqlite .prepare( "INSERT INTO project_associations (id, project_root_id, pattern) VALUES (?, ?, ?)", ) .run("association-1", "assigned-root", "/Users/test/code/a2b/**"); sqlite .prepare( "INSERT INTO timeline_events (id, project_root_id) VALUES (?, ?), (?, ?)", ) .run("event-1", "remote-root", "event-2", "assigned-root"); sqlite .prepare( "INSERT INTO live_activity_sessions (id, project_root_id) VALUES (?, ?)", ) .run("session-1", "assigned-root"); const result = deduplicateProjectRootsByPath(sqlite); expect(result).toMatchObject({ groupsMerged: 1, rootsRemoved: 1, associationsRepointed: 1, eventsRepointed: 1, liveSessionsRepointed: 1, skippedRemoteConflicts: 0, skippedProjectConflicts: 0, }); expect(sqlite.prepare("SELECT * FROM project_roots").all()).toEqual([ { id: "remote-root", scan_root_id: "git-scan", project_id: "project-a2b", device_id: null, path: "/Users/test/code/a2b", detected_name: "a2b", git_remote: "github.com/b-open-io/a2b", peacock_color: "blue", created_at: 2, }, ]); expect( sqlite .prepare("SELECT id, project_root_id FROM timeline_events ORDER BY id") .all(), ).toEqual([ { id: "event-1", project_root_id: "remote-root" }, { id: "event-2", project_root_id: "remote-root" }, ]); expect( sqlite.prepare("SELECT project_root_id FROM project_associations").get(), ).toEqual({ project_root_id: "remote-root" }); expect( sqlite .prepare("SELECT project_root_id FROM live_activity_sessions") .get(), ).toEqual({ project_root_id: "remote-root" }); expect(deduplicateProjectRootsByPath(sqlite).groupsMerged).toBe(0); }); test("retains duplicates with conflicting project assignments", () => { const insert = sqlite.prepare( `INSERT INTO project_roots (id, project_id, path, detected_name, created_at) VALUES (?, ?, ?, ?, ?)`, ); insert.run("root-a", "project-a", "/same/path", "same", 1); insert.run("root-b", "project-b", "/same/path", "same", 2); const result = deduplicateProjectRootsByPath(sqlite); expect(result.skippedProjectConflicts).toBe(1); expect(result.rootsRemoved).toBe(0); expect( sqlite.prepare("SELECT COUNT(*) AS count FROM project_roots").get(), ).toEqual({ count: 2 }); }); test("does not merge the same path from different devices", () => { const insert = sqlite.prepare( `INSERT INTO project_roots (id, device_id, path, detected_name, created_at) VALUES (?, ?, ?, ?, ?)`, ); insert.run("root-a", "device-a", "/same/path", "same", 1); insert.run("root-b", "device-b", "/same/path", "same", 2); const result = deduplicateProjectRootsByPath(sqlite); expect(result.groupsMerged).toBe(0); expect( sqlite.prepare("SELECT COUNT(*) AS count FROM project_roots").get(), ).toEqual({ count: 2 }); }); });