import { test, mock } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const require = createRequire(import.meta.url); // The app main process is now an ES module. It runs side-effectfully on import // (registers ipcMain handlers, opens windows, etc.) and has no exports, so we // mock its ESM dependencies with node:test's `mock.module` and load it via a // cache-busted dynamic import. // // `mock.module` keys mocks by the *resolved* module URL. The app's dependencies // live in `app/node_modules`, so they are NOT resolvable from this test file's // directory, and bare specifiers ('electron', ...) would either fail to resolve // here or resolve to the wrong ESM entry (e.g. chokidar exposes esm/index.js via // its "exports" map, which differs from require.resolve's CJS entry). We instead // resolve each bare specifier exactly as the main module sees it (ESM resolution // relative to the main module's directory) and mock that URL. Relative deps are // resolved against the main module URL directly. const mainUrl = new URL('../app/src/main/index.ts', import.meta.url); const mainPath = fileURLToPath(mainUrl); const mainDir = fileURLToPath(new URL('.', mainUrl)); function esmResolve(specifier) { return execFileSync( process.execPath, ['--input-type=module', '-e', `process.stdout.write(import.meta.resolve(${JSON.stringify(specifier)}))`], { cwd: mainDir, encoding: 'utf8' }, ).trim(); } const ELECTRON_URL = esmResolve('electron'); const DATABASE_URL = esmResolve('better-sqlite3'); const CHOKIDAR_URL = esmResolve('chokidar'); const INDEXER_URL = new URL('./indexer.ts', mainUrl).href; const INDEXER_SERVICE_URL = new URL('./indexer-service.ts', mainUrl).href; const INDEXER_WORKER_URL = new URL('./indexer-worker-client.ts', mainUrl).href; let importCounter = 0; // Registers the given [specifier, options] mocks and returns a restore fn. function registerMocks(defs) { const contexts = defs.map(([spec, opts]) => mock.module(spec, opts)); return () => { for (const ctx of contexts) ctx.restore(); mock.reset(); }; } // Fresh evaluation of the main module every call (cache-busted query string). async function importMain() { await import(`${mainUrl.href}?t=${++importCounter}-${Date.now()}`); await new Promise(resolve => setImmediate(resolve)); } // Electron named-export namespace. ESM named imports are validated at load time, // so every export the app imports ('app', 'BrowserWindow', 'ipcMain', 'clipboard', // 'dialog', 'nativeImage', 'shell') must be present, even if unused by a test. function electronNamespace({ app, BrowserWindow, ipcMain }) { return { app: app ?? { whenReady: () => Promise.resolve(), on() {}, quit() {} }, BrowserWindow, ipcMain: ipcMain ?? { handle() {} }, clipboard: {}, dialog: {}, nativeImage: {}, shell: {}, }; } function noopChokidar() { return { watch: () => ({ on() { return this; }, close() {} }) }; } function defaultIndexerService() { return { createIndexerService: () => ({ start() {}, stop() {}, idle: async () => {}, runBuildNow() { return Promise.resolve(); }, }), }; } function defaultIndexerWorkerClient() { return { createWorkerBuildIndex: () => ({ buildIndex: async () => ({ files: 0, affectedSessionIds: [] }), stop() {}, }), }; } async function loadMainForWindowFlags(flags) { const originalArgv = process.argv; const originalHome = process.env.HOME; const home = join(tmpdir(), `obelisk-window-flags-${Date.now()}-${Math.random()}`); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); process.env.HOME = home; process.argv = [originalArgv[0] || 'node', originalArgv[1] || 'electron', ...flags]; const windows = []; class FakeDatabase { pragma() {} close() {} prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor(options) { this.options = options; this.loadedURL = null; this.loadedFile = null; this.devToolsOpened = false; this.webContents = { on() {}, setZoomLevel() {}, openDevTools: () => { this.devToolsOpened = true; }, send() {}, }; windows.push(this); } loadFile(filePath) { this.loadedFile = filePath; } loadURL(url) { this.loadedURL = url; return Promise.resolve(); } close() {} static getAllWindows() { return windows; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow }) }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }], [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }], ]); try { await importMain(); return windows; } finally { restore(); process.argv = originalArgv; process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } } test('dev mode does not open DevTools unless explicitly requested', async () => { const packagedWindows = await loadMainForWindowFlags([]); assert.equal(packagedWindows.length, 1); assert.equal(packagedWindows[0].loadedURL, null); assert.equal(packagedWindows[0].options.webPreferences.devTools, false); assert.equal(packagedWindows[0].devToolsOpened, false); const devWindows = await loadMainForWindowFlags(['--dev']); assert.equal(devWindows.length, 1); assert.equal(devWindows[0].loadedURL, 'http://localhost:5173'); assert.equal(devWindows[0].options.webPreferences.devTools, true); assert.equal(devWindows[0].devToolsOpened, false); const devtoolsWindows = await loadMainForWindowFlags(['--dev', '--devtools']); assert.equal(devtoolsWindows.length, 1); assert.equal(devtoolsWindows[0].loadedURL, 'http://localhost:5173'); assert.equal(devtoolsWindows[0].devToolsOpened, true); }); test('main process watches Codex sessions directory instead of Codex root', async () => { const originalHome = process.env.HOME; const home = join(tmpdir(), `obelisk-main-watch-dirs-${Date.now()}`); const claudeDir = join(home, '.claude'); const codexDir = join(home, '.codex'); mkdirSync(join(claudeDir, 'projects'), { recursive: true }); mkdirSync(join(codexDir, 'sessions'), { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); process.env.HOME = home; const serviceOptions = []; class FakeDatabase { pragma() {} close() {} prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow }) }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: { createIndexerService: (options) => { serviceOptions.push(options); return { start() {}, stop() {}, idle: async () => {}, runBuildNow() { return Promise.resolve(); }, }; }, }, }], [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }], ]); try { await importMain(); assert.equal(serviceOptions.length, 1); assert.deepEqual(serviceOptions[0].watchDirs, [ join(claudeDir, 'projects'), join(codexDir, 'sessions'), ]); assert.equal(serviceOptions[0].watchDirs.includes(codexDir), false); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } }); test('session IPC hides Codex rows by default and supports explicit source opt-in', async () => { const originalHome = process.env.HOME; const home = join(tmpdir(), `obelisk-main-source-filter-${Date.now()}`); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); process.env.HOME = home; const ipcHandlers = new Map(); const queries = []; class FakeDatabase { pragma() {} close() {} prepare(sql) { return { all: (...params) => { queries.push({ sql, params }); return []; }, get: (...params) => { queries.push({ sql, params }); return null; }, run: () => ({}), }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow, ipcMain: { handle(channel, handler) { ipcHandlers.set(channel, handler); }, }, }), }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }], [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }], ]); try { await importMain(); ipcHandlers.get('db:getSessions')(null, {}); assert.match(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = 'claude'/); ipcHandlers.get('db:getProjects')(null, {}); assert.match(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = 'claude'/); ipcHandlers.get('db:getSessions')(null, { includeCodex: true }); assert.doesNotMatch(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = 'claude'/); ipcHandlers.get('db:getSessions')(null, { source: 'codex' }); assert.match(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = \?/); assert.ok(queries.at(-1).params.includes('codex')); await ipcHandlers.get('settings:get')(); assert.ok( queries.some(q => /COUNT\(\*\) as c FROM sessions WHERE COALESCE\(source, 'claude'\) = 'claude'/.test(q.sql)), ); assert.ok( queries.some(q => /MAX\(started_at\) as t FROM sessions WHERE COALESCE\(source, 'claude'\) = 'claude'/.test(q.sql)), ); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } }); test('main process migrates an existing app database before source-filtered IPC queries', async () => { const originalHome = process.env.HOME; const home = join(tmpdir(), `obelisk-main-db-migration-${Date.now()}`); const obeliskDir = join(home, '.obelisk'); mkdirSync(obeliskDir, { recursive: true }); process.env.HOME = home; const { DatabaseSync } = require('node:sqlite'); const dbPath = join(obeliskDir, 'obelisk.sqlite'); const legacy = new DatabaseSync(dbPath); legacy.exec(` CREATE TABLE sessions ( id TEXT PRIMARY KEY, title TEXT, project TEXT, project_path TEXT, started_at TEXT, ended_at TEXT, git_branch TEXT, version TEXT, message_count INTEGER DEFAULT 0, jsonl_path TEXT ); CREATE TABLE messages ( uuid TEXT PRIMARY KEY, session_id TEXT, type TEXT, parent_uuid TEXT, timestamp TEXT, role TEXT, text TEXT, content_type TEXT, is_meta INTEGER DEFAULT 0, model TEXT, is_sidechain INTEGER DEFAULT 0, agent_id TEXT, input_tokens INTEGER, output_tokens INTEGER, cwd TEXT, skill TEXT, turn_duration_ms INTEGER ); CREATE TABLE memories ( id TEXT PRIMARY KEY, session_id TEXT, project TEXT, message_start TEXT, message_end TEXT, path TEXT, anchors TEXT, summary TEXT, created_at TEXT, deleted_at TEXT, deleted_reason TEXT ); INSERT INTO sessions (id, title, project, started_at, message_count) VALUES ('legacy-session', 'Legacy session', 'quiet-zero', '2026-06-10T10:00:00Z', 1); `); legacy.close(); const ipcHandlers = new Map(); // better-sqlite3-compatible adapter over node:sqlite so the real migration // logic (ALTER TABLE ADD COLUMN source, etc.) runs against a real database. class SqliteCompatDatabase { constructor(dbFile) { this.db = new DatabaseSync(dbFile); } pragma(statement) { this.db.exec(`PRAGMA ${statement}`); } exec(sql) { return this.db.exec(sql); } close() { return this.db.close(); } prepare(sql) { const stmt = this.db.prepare(sql); return { all: (...params) => stmt.all(...params), get: (...params) => stmt.get(...params), run: (...params) => stmt.run(...params), }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow, ipcMain: { handle(channel, handler) { ipcHandlers.set(channel, handler); }, }, }), }], [DATABASE_URL, { defaultExport: SqliteCompatDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }], [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }], ]); try { await importMain(); const sessions = ipcHandlers.get('db:getSessions')(null, {}); assert.equal(sessions[0].id, 'legacy-session'); assert.equal(sessions[0].source, 'claude'); assert.deepEqual(ipcHandlers.get('db:getStats')(null, {}), { sessions: 1, memories: 0, memoriesArchived: 0, }); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } }); test('closing the last macOS window releases background resources until activation', async () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); const originalHome = process.env.HOME; const home = join(tmpdir(), `obelisk-main-window-${Date.now()}`); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); process.env.HOME = home; Object.defineProperty(process, 'platform', { value: 'darwin' }); const appHandlers = new Map(); const serviceEvents = []; const workers = []; const watchers = []; const windows = []; let quitCalled = false; class FakeDatabase { pragma() {} close() { serviceEvents.push('db-close'); } prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; windows.push(this); } loadFile() {} loadURL() {} close() {} static getAllWindows() { return windows; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ app: { whenReady: () => Promise.resolve(), on(event, handler) { appHandlers.set(event, handler); }, quit() { quitCalled = true; }, }, BrowserWindow: FakeBrowserWindow, }), }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: { watch: () => { const watcher = { on() { return this; }, close() { serviceEvents.push('watcher-close'); } }; watchers.push(watcher); return watcher; }, }, }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: { createIndexerService: () => ({ start() { serviceEvents.push('service-start'); }, stop() { serviceEvents.push('service-stop'); }, idle: async () => { serviceEvents.push('service-idle'); }, runBuildNow() { serviceEvents.push('service-build'); return Promise.resolve(); }, }), }, }], [INDEXER_WORKER_URL, { namedExports: { createWorkerBuildIndex: () => { const worker = { stop() { serviceEvents.push('worker-stop'); } }; workers.push(worker); return worker; }, }, }], ]); try { await importMain(); assert.equal(windows.length, 1); assert.equal(workers.length, 1); assert.equal(watchers.length, 1); windows.length = 0; appHandlers.get('window-all-closed')(); await new Promise(resolve => setImmediate(resolve)); assert.equal(quitCalled, false); assert.ok(serviceEvents.includes('service-stop')); assert.ok(serviceEvents.includes('worker-stop')); assert.ok(serviceEvents.includes('watcher-close')); assert.ok(serviceEvents.includes('db-close')); appHandlers.get('activate')(); await new Promise(resolve => setImmediate(resolve)); assert.equal(windows.length, 1); assert.equal(workers.length, 2); assert.equal(watchers.length, 2); assert.equal(serviceEvents.filter(e => e === 'service-start').length, 2); } finally { restore(); process.env.HOME = originalHome; if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform); rmSync(home, { recursive: true, force: true }); } }); test('settings rebuild reopens the database from the configured Claude path', async () => { const home = join(tmpdir(), `obelisk-main-settings-${Date.now()}`); const defaultClaudeDir = join(home, '.claude'); const customClaudeDir = join(home, 'custom-claude'); const customCodexDir = join(home, 'custom-codex'); mkdirSync(defaultClaudeDir, { recursive: true }); mkdirSync(customClaudeDir, { recursive: true }); mkdirSync(customCodexDir, { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(defaultClaudeDir, 'obelisk.sqlite'), ''); writeFileSync(join(customClaudeDir, 'obelisk.sqlite'), 'legacy custom db'); writeFileSync(join(home, '.obelisk', 'settings.json'), JSON.stringify({ claudeDir: customClaudeDir, codexDir: customCodexDir, })); const originalHome = process.env.HOME; process.env.HOME = home; const ipcHandlers = new Map(); const openedDbPaths = []; const buildCalls = []; const serviceEvents = []; class FakeDatabase { constructor(dbPath) { openedDbPaths.push(dbPath); } pragma() {} close() {} prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow, ipcMain: { handle(channel, handler) { ipcHandlers.set(channel, handler); }, }, }), }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: { createIndexerService: () => ({ start() { serviceEvents.push('start'); }, stop() { serviceEvents.push('stop'); }, idle: async () => { serviceEvents.push('idle'); }, runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, }), }, }], [INDEXER_WORKER_URL, { namedExports: { createWorkerBuildIndex: () => ({ buildIndex: async (args) => { serviceEvents.push('build'); buildCalls.push(args); writeFileSync(args.dbPath, 'rebuilt temp db'); return { files: 2, affectedSessionIds: ['session-1', 'session-2'] }; }, stop() { return Promise.resolve(); }, }), }, }], ]); try { await importMain(); const rebuild = ipcHandlers.get('settings:rebuildIndex'); assert.equal(typeof rebuild, 'function'); await rebuild(); assert.equal(buildCalls.at(-1).claudeDir, customClaudeDir); assert.equal(buildCalls.at(-1).projectsDir, join(customClaudeDir, 'projects')); assert.equal(buildCalls.at(-1).codexDir, customCodexDir); assert.notEqual(buildCalls.at(-1).dbPath, join(home, '.obelisk', 'obelisk.sqlite')); assert.equal(buildCalls.at(-1).preserveDbPath, join(home, '.obelisk', 'obelisk.sqlite')); assert.equal(openedDbPaths.at(-1), join(home, '.obelisk', 'obelisk.sqlite')); assert.equal( require('node:fs').readFileSync(join(home, '.obelisk', 'obelisk.sqlite'), 'utf8'), 'rebuilt temp db', ); assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop')); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } }); test('settings rebuild keeps the existing database after a worker failure', async () => { const home = join(tmpdir(), `obelisk-main-settings-rebuild-failure-${Date.now()}`); const customClaudeDir = join(home, 'custom-claude'); const customCodexDir = join(home, 'custom-codex'); mkdirSync(customClaudeDir, { recursive: true }); mkdirSync(customCodexDir, { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(customClaudeDir, 'obelisk.sqlite'), 'legacy custom db'); writeFileSync(join(home, '.obelisk', 'settings.json'), JSON.stringify({ claudeDir: customClaudeDir, codexDir: customCodexDir, })); const originalHome = process.env.HOME; process.env.HOME = home; const ipcHandlers = new Map(); const openedDbPaths = []; const closedDbPaths = []; const serviceEvents = []; class FakeDatabase { constructor(dbPath) { this.dbPath = dbPath; openedDbPaths.push(dbPath); } pragma() {} close() { closedDbPaths.push(this.dbPath); } prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow, ipcMain: { handle(channel, handler) { ipcHandlers.set(channel, handler); }, }, }), }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: { createIndexerService: () => ({ start() { serviceEvents.push('start'); }, stop() { serviceEvents.push('stop'); }, idle: async () => { serviceEvents.push('idle'); }, runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, }), }, }], [INDEXER_WORKER_URL, { namedExports: { createWorkerBuildIndex: () => ({ buildIndex: async () => { serviceEvents.push('build'); throw new Error('worker exploded'); }, stop() {}, }), }, }], ]); try { await importMain(); const rebuild = ipcHandlers.get('settings:rebuildIndex'); const openCountBeforeRebuild = openedDbPaths.length; await assert.rejects(() => rebuild(), /worker exploded/); const expectedDbPath = join(home, '.obelisk', 'obelisk.sqlite'); assert.equal(openedDbPaths.at(-1), expectedDbPath); assert.equal(openedDbPaths.length, openCountBeforeRebuild); assert.equal(closedDbPaths.includes(expectedDbPath), false); assert.equal( require('node:fs').readFileSync(expectedDbPath, 'utf8'), 'legacy custom db', ); assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop')); assert.ok(serviceEvents.lastIndexOf('start') > serviceEvents.indexOf('build')); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } }); test('settings rebuild cancels an in-flight background build instead of waiting for it', async () => { const home = join(tmpdir(), `obelisk-main-settings-rebuild-cancel-${Date.now()}`); const customClaudeDir = join(home, 'custom-claude'); const customCodexDir = join(home, 'custom-codex'); mkdirSync(customClaudeDir, { recursive: true }); mkdirSync(customCodexDir, { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true }); writeFileSync(join(customClaudeDir, 'obelisk.sqlite'), 'legacy custom db'); writeFileSync(join(home, '.obelisk', 'settings.json'), JSON.stringify({ claudeDir: customClaudeDir, codexDir: customCodexDir, })); const originalHome = process.env.HOME; process.env.HOME = home; const ipcHandlers = new Map(); const serviceEvents = []; let buildIndexCalls = 0; class FakeDatabase { pragma() {} close() {} prepare() { return { get: () => null, all: () => [], run: () => ({}) }; } } class FakeBrowserWindow { constructor() { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; } loadFile() {} loadURL() {} close() {} static getAllWindows() { return []; } static fromWebContents() { return null; } } const restore = registerMocks([ [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow, ipcMain: { handle(channel, handler) { ipcHandlers.set(channel, handler); }, }, }), }], [DATABASE_URL, { defaultExport: FakeDatabase }], [CHOKIDAR_URL, { defaultExport: noopChokidar() }], [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }], [INDEXER_SERVICE_URL, { namedExports: { createIndexerService: () => ({ start() { serviceEvents.push('start'); }, stop() { serviceEvents.push('stop'); }, idle: async () => new Promise(() => {}), runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, }), }, }], [INDEXER_WORKER_URL, { namedExports: { createWorkerBuildIndex: () => ({ buildIndex: async (args) => { serviceEvents.push(`build-${++buildIndexCalls}`); writeFileSync(args.dbPath, 'rebuilt temp db'); return { files: 2, affectedSessionIds: [] }; }, stop() { serviceEvents.push('worker-stop'); return Promise.resolve(); }, }), }, }], ]); try { await importMain(); const rebuild = ipcHandlers.get('settings:rebuildIndex'); const outcome = await Promise.race([ rebuild().then(() => 'done'), new Promise(resolve => setTimeout(() => resolve('timeout'), 20)), ]); assert.equal(outcome, 'done'); assert.ok(serviceEvents.indexOf('worker-stop') > serviceEvents.indexOf('stop')); assert.ok(serviceEvents.some(event => event.startsWith('build-'))); } finally { restore(); process.env.HOME = originalHome; rmSync(home, { recursive: true, force: true }); } });