Files
obelisk/tests/app-main-settings.test.mjs
T
tommy0103 80c125a572 build(app): migrate to electron-vite (main/preload/renderer), keep electron-builder (Phase 5d-3a)
Restructure app into src/{main,preload,renderer}; electron.vite.config.ts builds
all three (each main module its own input so CJS requires + the indexer worker
resolve; better-sqlite3 externalized; Vue plugin for renderer). main/index.js
paths updated for the out/ layout + ELECTRON_RENDERER_URL. Still JS/CJS — TS+ESM
and core consumption are the next stages. Verified: npm run dev launches clean;
electron-vite build succeeds; root suite 119/119.
2026-07-09 10:20:18 +08:00

985 lines
31 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Module from 'node:module';
const require = createRequire(import.meta.url);
async function loadMainForWindowFlags(flags) {
const originalArgv = process.argv;
const originalHome = process.env.HOME;
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
const home = join(tmpdir(), `obelisk-window-flags-${Date.now()}-${Math.random()}`);
mkdirSync(join(home, '.obelisk'), { recursive: true });
writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), '');
delete require.cache[mainPath];
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; }
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: { handle() {} },
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() {},
stop() {},
idle: async () => {},
runBuildNow() { return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => ({
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }),
stop() {},
}),
};
}
if (request === 'chokidar') {
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
return windows;
} finally {
Module._load = originalLoad;
delete require.cache[mainPath];
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 originalLoad = Module._load;
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 = [];
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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; }
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: { handle() {} },
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: (options) => {
serviceOptions.push(options);
return {
start() {},
stop() {},
idle: async () => {},
runBuildNow() { return Promise.resolve(); },
};
},
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => ({
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }),
stop() {},
}),
};
}
if (request === 'chokidar') return { watch: () => ({ on() { return this; }, close() {} }) };
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
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 originalLoad = Module._load;
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 = [];
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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; }
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: {
handle(channel, handler) {
ipcHandlers.set(channel, handler);
},
},
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() {},
stop() {},
idle: async () => {},
runBuildNow() { return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => ({
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }),
stop() {},
}),
};
}
if (request === 'chokidar') return { watch: () => ({ on() { return this; }, close() {} }) };
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
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 originalLoad = Module._load;
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();
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeBrowserWindow {
constructor() {
this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} };
}
loadFile() {}
loadURL() {}
close() {}
static getAllWindows() { return []; }
static fromWebContents() { return null; }
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: {
handle(channel, handler) {
ipcHandlers.set(channel, handler);
},
},
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') {
return 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),
};
}
};
}
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() {},
stop() {},
idle: async () => {},
runBuildNow() { return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => ({
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }),
stop() {},
}),
};
}
if (request === 'chokidar') return { watch: () => ({ on() { return this; }, close() {} }) };
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true });
}
});
test('closing the last macOS window releases background resources until activation', async () => {
const originalLoad = Module._load;
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'), '');
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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; }
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on(event, handler) { appHandlers.set(event, handler); },
quit() { quitCalled = true; },
},
BrowserWindow: FakeBrowserWindow,
ipcMain: { handle() {} },
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
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(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => {
const worker = { stop() { serviceEvents.push('worker-stop'); } };
workers.push(worker);
return worker;
},
};
}
if (request === 'chokidar') {
return {
watch: () => {
const watcher = { on() { return this; }, close() { serviceEvents.push('watcher-close'); } };
watchers.push(watcher);
return watcher;
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
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 = [];
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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;
}
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: {
handle(channel, handler) {
ipcHandlers.set(channel, handler);
},
},
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); },
idle: async () => { serviceEvents.push('idle'); },
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
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(); },
}),
};
}
if (request === 'chokidar') {
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
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 = [];
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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;
}
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: {
handle(channel, handler) {
ipcHandlers.set(channel, handler);
},
},
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); },
idle: async () => { serviceEvents.push('idle'); },
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
return {
createWorkerBuildIndex: () => ({
buildIndex: async () => {
serviceEvents.push('build');
throw new Error('worker exploded');
},
stop() {},
}),
};
}
if (request === 'chokidar') {
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
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 = [];
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
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;
}
}
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow,
ipcMain: {
handle(channel, handler) {
ipcHandlers.set(channel, handler);
},
},
clipboard: {},
dialog: {},
nativeImage: {},
};
}
if (request === 'better-sqlite3') return FakeDatabase;
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') {
return {
createIndexerService: () => ({
start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); },
idle: async () => new Promise(() => {}),
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}),
};
}
if (request === './indexer-worker-client') {
let buildIndexCalls = 0;
return {
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();
},
}),
};
}
if (request === 'chokidar') {
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try {
require('../app/src/main/index.js');
await new Promise(resolve => setImmediate(resolve));
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 {
Module._load = originalLoad;
delete require.cache[mainPath];
process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true });
}
});