refactor(app): migrate to ESM; preload emitted as CJS for sandbox (Phase 5d-3b)

App source (main/preload/worker/renderer) -> ESM; app is now type: module;
__dirname via import.meta.url; worker spawned with type module. Preload is built
as CJS (electron-vite output format) because the sandboxed renderer does not
support ESM preload; main loads ../preload/index.js. Removed dead imports
(nativeImage, readline) and the obsolete scripts/dev.js.

Tests: 4 app tests require->import; app-main-settings rewritten with node:test
mock.module + dynamic import (replacing CJS Module._load mocking); test script
adds --experimental-test-module-mocks. electron-vite build clean, 119/119, and
npm run dev verified: app launches, preload bridges IPC, data loads.
This commit is contained in:
tommy0103
2026-07-09 11:05:00 +08:00
parent 80c125a572
commit 905c10789a
16 changed files with 313 additions and 555 deletions
+7
View File
@@ -24,6 +24,13 @@ export default defineConfig({
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
// Electron sandbox does not support ESM preload — emit CJS index.js
// (main loads ../preload/index.js) even though the project is ESM.
output: { format: 'cjs', entryFileNames: '[name].js' },
},
},
}, },
renderer: { renderer: {
plugins: [vue()], plugins: [vue()],
+3 -5
View File
@@ -46,16 +46,13 @@
"out/**" "out/**"
], ],
"asarUnpack": [ "asarUnpack": [
"**/node_modules/better-sqlite3/**" "node_modules/better-sqlite3/**/*"
], ],
"extraResources": [ "extraResources": [
{ {
"from": "../scripts/schema.sql", "from": "../scripts/schema.sql",
"to": "scripts/schema.sql" "to": "scripts/schema.sql"
} }
],
"asarUnpack": [
"node_modules/better-sqlite3/**/*"
] ]
}, },
"dependencies": { "dependencies": {
@@ -70,5 +67,6 @@
"vite": "^6.0.0", "vite": "^6.0.0",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
} },
"type": "module"
} }
-138
View File
@@ -1,138 +0,0 @@
const { spawn } = require('child_process');
const http = require('http');
const net = require('net');
const path = require('path');
const appDir = path.join(__dirname, '..');
const binExt = process.platform === 'win32' ? '.cmd' : '';
const viteBin = path.join(appDir, 'node_modules', '.bin', `vite${binExt}`);
const electronBin = path.join(appDir, 'node_modules', '.bin', `electron${binExt}`);
const DEFAULT_DEV_PORT = Number(process.env.OBELISK_DEV_SERVER_PORT || 5173);
let viteProcess = null;
let electronProcess = null;
let shuttingDown = false;
function spawnLocal(command, args, extraEnv = {}) {
return spawn(command, args, {
cwd: appDir,
stdio: 'inherit',
env: { ...process.env, ...extraEnv },
shell: process.platform === 'win32',
});
}
function waitForDevServer(url, timeoutMs = 20000) {
const started = Date.now();
return new Promise((resolve, reject) => {
const poll = () => {
const req = http.get(url, (res) => {
res.resume();
if (res.statusCode >= 200 && res.statusCode < 400) {
resolve();
} else {
if (Date.now() - started >= timeoutMs) {
reject(new Error(`Timed out waiting for ${url}`));
} else {
setTimeout(poll, 250);
}
}
});
req.on('error', () => {
if (Date.now() - started >= timeoutMs) {
reject(new Error(`Timed out waiting for ${url}`));
return;
}
setTimeout(poll, 250);
});
req.setTimeout(1000, () => {
req.destroy();
});
};
poll();
});
}
function isDevServerRunning(url) {
return new Promise((resolve) => {
const req = http.get(url, (res) => {
res.resume();
resolve(res.statusCode >= 200 && res.statusCode < 400);
});
req.on('error', () => resolve(false));
req.setTimeout(1000, () => {
req.destroy();
resolve(false);
});
});
}
function isPortAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close(() => resolve(true));
});
server.listen(port, '127.0.0.1');
});
}
async function findAvailablePort(startPort) {
for (let port = startPort; port < startPort + 20; port++) {
if (await isPortAvailable(port)) return port;
}
throw new Error(`No available port found from ${startPort} to ${startPort + 19}`);
}
function stopChild(child) {
if (!child || child.killed) return;
child.kill(process.platform === 'win32' ? undefined : 'SIGTERM');
}
function shutdown(exitCode = 0) {
if (shuttingDown) return;
shuttingDown = true;
stopChild(electronProcess);
stopChild(viteProcess);
process.exit(exitCode);
}
async function main() {
let devPort = DEFAULT_DEV_PORT;
let devUrl = `http://127.0.0.1:${devPort}`;
const existingServer = await isDevServerRunning(devUrl);
if (existingServer) {
console.log(`Using existing Vite dev server at ${devUrl}`);
} else {
devPort = await findAvailablePort(DEFAULT_DEV_PORT);
devUrl = `http://127.0.0.1:${devPort}`;
viteProcess = spawnLocal(viteBin, ['renderer', '--host', '127.0.0.1', '--port', String(devPort), '--strictPort']);
viteProcess.on('exit', (code, signal) => {
if (!shuttingDown && !electronProcess) shutdown(code || (signal ? 1 : 0));
});
try {
await waitForDevServer(devUrl);
} catch (error) {
console.error(error.message);
shutdown(1);
return;
}
}
electronProcess = spawnLocal(electronBin, ['.', '--dev', ...process.argv.slice(2)], {
OBELISK_DEV_SERVER_URL: devUrl,
});
electronProcess.on('exit', (code, signal) => {
shutdown(code || (signal ? 1 : 0));
});
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
main().catch((error) => {
console.error(error);
shutdown(1);
});
+13 -12
View File
@@ -1,12 +1,16 @@
const { app, BrowserWindow, ipcMain, clipboard, dialog, nativeImage } = require('electron'); import { app, BrowserWindow, ipcMain, clipboard, dialog, shell } from 'electron';
const path = require('path'); import path from 'node:path';
const os = require('os'); import os from 'node:os';
const fs = require('fs'); import fs from 'node:fs';
const Database = require('better-sqlite3'); import { fileURLToPath } from 'node:url';
const { writeHeartbeat } = require('./indexer'); import Database from 'better-sqlite3';
const { createIndexerService } = require('./indexer-service'); import chokidar from 'chokidar';
const { createWorkerBuildIndex } = require('./indexer-worker-client'); import { writeHeartbeat } from './indexer.js';
const { buildRecapExportQuery } = require('./recap-capture-query'); import { createIndexerService } from './indexer-service.js';
import { createWorkerBuildIndex } from './indexer-worker-client.js';
import { buildRecapExportQuery } from './recap-capture-query.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function detectClaudeDir() { function detectClaudeDir() {
// macOS / Linux: ~/.claude // macOS / Linux: ~/.claude
@@ -283,7 +287,6 @@ let obeliskWatcher = null;
function startObeliskWatcher() { function startObeliskWatcher() {
if (obeliskWatcher) return obeliskWatcher; if (obeliskWatcher) return obeliskWatcher;
const chokidar = require('chokidar');
if (!fs.existsSync(OBELISK_DIR)) { if (!fs.existsSync(OBELISK_DIR)) {
fs.mkdirSync(OBELISK_DIR, { recursive: true }); fs.mkdirSync(OBELISK_DIR, { recursive: true });
} }
@@ -488,7 +491,6 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
if (!jsonlPath || !fs.existsSync(jsonlPath)) return null; if (!jsonlPath || !fs.existsSync(jsonlPath)) return null;
// Scan JSONL for the message UUID and extract full text // Scan JSONL for the message UUID and extract full text
const readline = require('readline');
const data = fs.readFileSync(jsonlPath, 'utf-8'); const data = fs.readFileSync(jsonlPath, 'utf-8');
const lines = data.split('\n'); const lines = data.split('\n');
for (const line of lines) { for (const line of lines) {
@@ -809,7 +811,6 @@ ipcMain.handle('settings:browseFolder', async (event) => {
}); });
ipcMain.handle('settings:revealPath', (_, p) => { ipcMain.handle('settings:revealPath', (_, p) => {
const { shell } = require('electron');
if (fs.existsSync(p)) shell.showItemInFolder(p); if (fs.existsSync(p)) shell.showItemInFolder(p);
}); });
+6 -5
View File
@@ -1,6 +1,7 @@
const fs = require('fs'); import fs from 'node:fs';
const os = require('os'); import os from 'node:os';
const path = require('path'); import path from 'node:path';
import chokidarModule from 'chokidar';
const DEFAULT_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects'); const DEFAULT_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
const DEFAULT_DEBOUNCE_MS = 2000; const DEFAULT_DEBOUNCE_MS = 2000;
@@ -38,7 +39,7 @@ function createIndexerService({
if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name); if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name);
}; };
for (const root of existingRoots) { for (const root of existingRoots) {
const watcher = (chokidar || require('chokidar')).watch(root, { const watcher = (chokidar || chokidarModule).watch(root, {
cwd: root, cwd: root,
ignoreInitial: true, ignoreInitial: true,
awaitWriteFinish: { awaitWriteFinish: {
@@ -192,4 +193,4 @@ function createIndexerService({
}; };
} }
module.exports = { createIndexerService }; export { createIndexerService };
+7 -4
View File
@@ -1,5 +1,8 @@
const path = require('path'); import path from 'node:path';
const { Worker } = require('worker_threads'); import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function createWorkerBuildIndex({ function createWorkerBuildIndex({
workerPath = path.join(__dirname, 'indexer-worker.js'), workerPath = path.join(__dirname, 'indexer-worker.js'),
@@ -16,7 +19,7 @@ function createWorkerBuildIndex({
const ensureWorker = () => { const ensureWorker = () => {
if (worker) return worker; if (worker) return worker;
worker = new WorkerImpl(workerPath); worker = new WorkerImpl(workerPath, { type: 'module' });
worker.on('message', (message) => { worker.on('message', (message) => {
const current = pending.get(message.id); const current = pending.get(message.id);
if (!current) return; if (!current) return;
@@ -57,4 +60,4 @@ function createWorkerBuildIndex({
return { buildIndex, stop }; return { buildIndex, stop };
} }
module.exports = { createWorkerBuildIndex }; export { createWorkerBuildIndex };
+2 -2
View File
@@ -1,5 +1,5 @@
const { parentPort } = require('worker_threads'); import { parentPort } from 'node:worker_threads';
const { buildIndex } = require('./indexer'); import { buildIndex } from './indexer.js';
parentPort.on('message', ({ id, args }) => { parentPort.on('message', ({ id, args }) => {
try { try {
+8 -5
View File
@@ -1,7 +1,10 @@
const fs = require('fs'); import fs from 'node:fs';
const os = require('os'); import os from 'node:os';
const path = require('path'); import path from 'node:path';
const Database = require('better-sqlite3'); import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TEXT_LIMIT = 10000; const TEXT_LIMIT = 10000;
const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), '.claude'); const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), '.claude');
@@ -1161,7 +1164,7 @@ function buildIndex({
} }
} }
module.exports = { export {
buildIndex, buildIndex,
writeHeartbeat, writeHeartbeat,
openIndexDb, openIndexDb,
+2 -2
View File
@@ -1,4 +1,4 @@
const path = require('path'); import path from 'node:path';
function cleanRecapFilename(filename) { function cleanRecapFilename(filename) {
if (!filename) return ''; if (!filename) return '';
@@ -15,4 +15,4 @@ function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' } =
return params.toString(); return params.toString();
} }
module.exports = { buildRecapExportQuery, cleanRecapFilename }; export { buildRecapExportQuery, cleanRecapFilename };
+1 -1
View File
@@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron'); import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('obelisk', { contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts), getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts),
+1 -1
View File
@@ -6,7 +6,7 @@
"description": "Explicit memory infrastructure for coding agents — a queryable SQLite evidence layer over local Claude Code and Codex history, plus human-approved durable memory.", "description": "Explicit memory infrastructure for coding agents — a queryable SQLite evidence layer over local Claude Code and Codex history, plus human-approved durable memory.",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"test": "node --test tests/*.test.mjs", "test": "node --experimental-test-module-mocks --test tests/*.test.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "eslint .", "lint": "eslint .",
"build:core": "rm -rf dist && tsc -p tsconfig.build.json" "build:core": "rm -rf dist && tsc -p tsconfig.build.json"
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { createIndexerService } = require('../app/src/main/indexer-service.js'); import { createIndexerService } from '../app/src/main/indexer-service.js';
function manualTimers() { function manualTimers() {
const timers = new Set(); const timers = new Set();
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { createWorkerBuildIndex } = require('../app/src/main/indexer-worker-client.js'); import { createWorkerBuildIndex } from '../app/src/main/indexer-worker-client.js';
test('worker build client resolves build results from a worker thread', async () => { test('worker build client resolves build results from a worker thread', async () => {
const instances = []; const instances = [];
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { buildIndex } = require('../app/src/main/indexer.js'); import { buildIndex } from '../app/src/main/indexer.js';
const { DatabaseSync } = require('node:sqlite'); const { DatabaseSync } = require('node:sqlite');
class TestDatabase { class TestDatabase {
+252 -369
View File
@@ -1,22 +1,108 @@
import { test } from 'node:test'; import { test, mock } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import Module from 'node:module';
const require = createRequire(import.meta.url); 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.js', 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.js', mainUrl).href;
const INDEXER_SERVICE_URL = new URL('./indexer-service.js', mainUrl).href;
const INDEXER_WORKER_URL = new URL('./indexer-worker-client.js', 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) { async function loadMainForWindowFlags(flags) {
const originalArgv = process.argv; const originalArgv = process.argv;
const originalHome = process.env.HOME; 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()}`); const home = join(tmpdir(), `obelisk-window-flags-${Date.now()}-${Math.random()}`);
mkdirSync(join(home, '.obelisk'), { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true });
writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), '');
delete require.cache[mainPath];
process.env.HOME = home; process.env.HOME = home;
process.argv = [originalArgv[0] || 'node', originalArgv[1] || 'electron', ...flags]; process.argv = [originalArgv[0] || 'node', originalArgv[1] || 'electron', ...flags];
@@ -51,54 +137,20 @@ async function loadMainForWindowFlags(flags) {
static fromWebContents() { return null; } static fromWebContents() { return null; }
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow }) }],
return { [DATABASE_URL, { defaultExport: FakeDatabase }],
app: { [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
whenReady: () => Promise.resolve(), [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
on() {}, [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }],
quit() {}, [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
}, ]);
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 { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
return windows; return windows;
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.argv = originalArgv; process.argv = originalArgv;
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
@@ -125,7 +177,6 @@ test('dev mode does not open DevTools unless explicitly requested', async () =>
}); });
test('main process watches Codex sessions directory instead of Codex root', async () => { test('main process watches Codex sessions directory instead of Codex root', async () => {
const originalLoad = Module._load;
const originalHome = process.env.HOME; const originalHome = process.env.HOME;
const home = join(tmpdir(), `obelisk-main-watch-dirs-${Date.now()}`); const home = join(tmpdir(), `obelisk-main-watch-dirs-${Date.now()}`);
const claudeDir = join(home, '.claude'); const claudeDir = join(home, '.claude');
@@ -137,8 +188,6 @@ test('main process watches Codex sessions directory instead of Codex root', asyn
process.env.HOME = home; process.env.HOME = home;
const serviceOptions = []; const serviceOptions = [];
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeDatabase { class FakeDatabase {
pragma() {} pragma() {}
@@ -159,25 +208,13 @@ test('main process watches Codex sessions directory instead of Codex root', asyn
static fromWebContents() { return null; } static fromWebContents() { return null; }
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, { namedExports: electronNamespace({ BrowserWindow: FakeBrowserWindow }) }],
return { [DATABASE_URL, { defaultExport: FakeDatabase }],
app: { [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
whenReady: () => Promise.resolve(), [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
on() {}, [INDEXER_SERVICE_URL, {
quit() {}, namedExports: {
},
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) => { createIndexerService: (options) => {
serviceOptions.push(options); serviceOptions.push(options);
return { return {
@@ -187,23 +224,13 @@ test('main process watches Codex sessions directory instead of Codex root', asyn
runBuildNow() { return Promise.resolve(); }, runBuildNow() { return Promise.resolve(); },
}; };
}, },
}; },
} }],
if (request === './indexer-worker-client') { [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
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 { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
assert.equal(serviceOptions.length, 1); assert.equal(serviceOptions.length, 1);
assert.deepEqual(serviceOptions[0].watchDirs, [ assert.deepEqual(serviceOptions[0].watchDirs, [
@@ -212,15 +239,13 @@ test('main process watches Codex sessions directory instead of Codex root', asyn
]); ]);
assert.equal(serviceOptions[0].watchDirs.includes(codexDir), false); assert.equal(serviceOptions[0].watchDirs.includes(codexDir), false);
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
}); });
test('session IPC hides Codex rows by default and supports explicit source opt-in', async () => { 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 originalHome = process.env.HOME;
const home = join(tmpdir(), `obelisk-main-source-filter-${Date.now()}`); const home = join(tmpdir(), `obelisk-main-source-filter-${Date.now()}`);
mkdirSync(join(home, '.obelisk'), { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true });
@@ -229,8 +254,6 @@ test('session IPC hides Codex rows by default and supports explicit source opt-i
const ipcHandlers = new Map(); const ipcHandlers = new Map();
const queries = []; const queries = [];
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeDatabase { class FakeDatabase {
pragma() {} pragma() {}
@@ -261,52 +284,26 @@ test('session IPC hides Codex rows by default and supports explicit source opt-i
static fromWebContents() { return null; } static fromWebContents() { return null; }
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, {
return { namedExports: electronNamespace({
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow, BrowserWindow: FakeBrowserWindow,
ipcMain: { ipcMain: {
handle(channel, handler) { handle(channel, handler) {
ipcHandlers.set(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(); },
}), }),
}; }],
} [DATABASE_URL, { defaultExport: FakeDatabase }],
if (request === './indexer-worker-client') { [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
return { [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
createWorkerBuildIndex: () => ({ [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }],
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }), [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
stop() {}, ]);
}),
};
}
if (request === 'chokidar') return { watch: () => ({ on() { return this; }, close() {} }) };
return originalLoad.call(this, request, parent, isMain);
};
try { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
ipcHandlers.get('db:getSessions')(null, {}); ipcHandlers.get('db:getSessions')(null, {});
assert.match(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = 'claude'/); assert.match(queries.at(-1).sql, /COALESCE\(source, 'claude'\) = 'claude'/);
@@ -329,15 +326,13 @@ test('session IPC hides Codex rows by default and supports explicit source opt-i
queries.some(q => /MAX\(started_at\) as t FROM sessions WHERE COALESCE\(source, 'claude'\) = 'claude'/.test(q.sql)), queries.some(q => /MAX\(started_at\) as t FROM sessions WHERE COALESCE\(source, 'claude'\) = 'claude'/.test(q.sql)),
); );
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
}); });
test('main process migrates an existing app database before source-filtered IPC queries', async () => { test('main process migrates an existing app database before source-filtered IPC queries', async () => {
const originalLoad = Module._load;
const originalHome = process.env.HOME; const originalHome = process.env.HOME;
const home = join(tmpdir(), `obelisk-main-db-migration-${Date.now()}`); const home = join(tmpdir(), `obelisk-main-db-migration-${Date.now()}`);
const obeliskDir = join(home, '.obelisk'); const obeliskDir = join(home, '.obelisk');
@@ -373,41 +368,10 @@ test('main process migrates an existing app database before source-filtered IPC
legacy.close(); legacy.close();
const ipcHandlers = new Map(); const ipcHandlers = new Map();
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeBrowserWindow { // better-sqlite3-compatible adapter over node:sqlite so the real migration
constructor() { // logic (ALTER TABLE ADD COLUMN source, etc.) runs against a real database.
this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} }; class SqliteCompatDatabase {
}
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) { constructor(dbFile) {
this.db = new DatabaseSync(dbFile); this.db = new DatabaseSync(dbFile);
} }
@@ -428,34 +392,39 @@ test('main process migrates an existing app database before source-filtered IPC
run: (...params) => stmt.run(...params), run: (...params) => stmt.run(...params),
}; };
} }
};
} }
if (request === './indexer') return { writeHeartbeat() {} };
if (request === './indexer-service') { class FakeBrowserWindow {
return { constructor() {
createIndexerService: () => ({ this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} };
start() {}, }
stop() {}, loadFile() {}
idle: async () => {}, loadURL() {}
runBuildNow() { return Promise.resolve(); }, 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 }],
if (request === './indexer-worker-client') { [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
return { [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
createWorkerBuildIndex: () => ({ [INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }],
buildIndex: async () => ({ files: 0, affectedSessionIds: [] }), [INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
stop() {}, ]);
}),
};
}
if (request === 'chokidar') return { watch: () => ({ on() { return this; }, close() {} }) };
return originalLoad.call(this, request, parent, isMain);
};
try { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
const sessions = ipcHandlers.get('db:getSessions')(null, {}); const sessions = ipcHandlers.get('db:getSessions')(null, {});
assert.equal(sessions[0].id, 'legacy-session'); assert.equal(sessions[0].id, 'legacy-session');
@@ -466,22 +435,18 @@ test('main process migrates an existing app database before source-filtered IPC
memoriesArchived: 0, memoriesArchived: 0,
}); });
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
}); });
test('closing the last macOS window releases background resources until activation', async () => { test('closing the last macOS window releases background resources until activation', async () => {
const originalLoad = Module._load;
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
const originalHome = process.env.HOME; const originalHome = process.env.HOME;
const home = join(tmpdir(), `obelisk-main-window-${Date.now()}`); const home = join(tmpdir(), `obelisk-main-window-${Date.now()}`);
mkdirSync(join(home, '.obelisk'), { recursive: true }); mkdirSync(join(home, '.obelisk'), { recursive: true });
writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), ''); writeFileSync(join(home, '.obelisk', 'obelisk.sqlite'), '');
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
process.env.HOME = home; process.env.HOME = home;
Object.defineProperty(process, 'platform', { value: 'darwin' }); Object.defineProperty(process, 'platform', { value: 'darwin' });
@@ -512,57 +477,51 @@ test('closing the last macOS window releases background resources until activati
static fromWebContents() { return null; } static fromWebContents() { return null; }
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, {
return { namedExports: electronNamespace({
app: { app: {
whenReady: () => Promise.resolve(), whenReady: () => Promise.resolve(),
on(event, handler) { appHandlers.set(event, handler); }, on(event, handler) { appHandlers.set(event, handler); },
quit() { quitCalled = true; }, quit() { quitCalled = true; },
}, },
BrowserWindow: FakeBrowserWindow, BrowserWindow: FakeBrowserWindow,
ipcMain: { handle() {} }, }),
clipboard: {}, }],
dialog: {}, [DATABASE_URL, { defaultExport: FakeDatabase }],
nativeImage: {}, [CHOKIDAR_URL, {
}; defaultExport: {
} watch: () => {
if (request === 'better-sqlite3') return FakeDatabase; const watcher = { on() { return this; }, close() { serviceEvents.push('watcher-close'); } };
if (request === './indexer') return { writeHeartbeat() {} }; watchers.push(watcher);
if (request === './indexer-service') { return watcher;
return { },
},
}],
[INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
[INDEXER_SERVICE_URL, {
namedExports: {
createIndexerService: () => ({ createIndexerService: () => ({
start() { serviceEvents.push('service-start'); }, start() { serviceEvents.push('service-start'); },
stop() { serviceEvents.push('service-stop'); }, stop() { serviceEvents.push('service-stop'); },
idle: async () => { serviceEvents.push('service-idle'); }, idle: async () => { serviceEvents.push('service-idle'); },
runBuildNow() { serviceEvents.push('service-build'); return Promise.resolve(); }, runBuildNow() { serviceEvents.push('service-build'); return Promise.resolve(); },
}), }),
}; },
} }],
if (request === './indexer-worker-client') { [INDEXER_WORKER_URL, {
return { namedExports: {
createWorkerBuildIndex: () => { createWorkerBuildIndex: () => {
const worker = { stop() { serviceEvents.push('worker-stop'); } }; const worker = { stop() { serviceEvents.push('worker-stop'); } };
workers.push(worker); workers.push(worker);
return 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 { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
assert.equal(windows.length, 1); assert.equal(windows.length, 1);
assert.equal(workers.length, 1); assert.equal(workers.length, 1);
@@ -586,8 +545,7 @@ test('closing the last macOS window releases background resources until activati
assert.equal(watchers.length, 2); assert.equal(watchers.length, 2);
assert.equal(serviceEvents.filter(e => e === 'service-start').length, 2); assert.equal(serviceEvents.filter(e => e === 'service-start').length, 2);
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform); if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform);
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
@@ -617,9 +575,6 @@ test('settings rebuild reopens the database from the configured Claude path', as
const openedDbPaths = []; const openedDbPaths = [];
const buildCalls = []; const buildCalls = [];
const serviceEvents = []; const serviceEvents = [];
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeDatabase { class FakeDatabase {
constructor(dbPath) { constructor(dbPath) {
@@ -634,57 +589,41 @@ test('settings rebuild reopens the database from the configured Claude path', as
class FakeBrowserWindow { class FakeBrowserWindow {
constructor() { constructor() {
this.webContents = { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} };
on() {},
setZoomLevel() {},
openDevTools() {},
send() {},
};
} }
loadFile() {} loadFile() {}
loadURL() {} loadURL() {}
close() {} close() {}
static getAllWindows() { static getAllWindows() { return []; }
return []; static fromWebContents() { return null; }
}
static fromWebContents() {
return null;
}
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, {
return { namedExports: electronNamespace({
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow, BrowserWindow: FakeBrowserWindow,
ipcMain: { ipcMain: {
handle(channel, handler) { handle(channel, handler) {
ipcHandlers.set(channel, handler); ipcHandlers.set(channel, handler);
}, },
}, },
clipboard: {}, }),
dialog: {}, }],
nativeImage: {}, [DATABASE_URL, { defaultExport: FakeDatabase }],
}; [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
} [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
if (request === 'better-sqlite3') return FakeDatabase; [INDEXER_SERVICE_URL, {
if (request === './indexer') return { writeHeartbeat() {} }; namedExports: {
if (request === './indexer-service') {
return {
createIndexerService: () => ({ createIndexerService: () => ({
start() { serviceEvents.push('start'); }, start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); }, stop() { serviceEvents.push('stop'); },
idle: async () => { serviceEvents.push('idle'); }, idle: async () => { serviceEvents.push('idle'); },
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}), }),
}; },
} }],
if (request === './indexer-worker-client') { [INDEXER_WORKER_URL, {
return { namedExports: {
createWorkerBuildIndex: () => ({ createWorkerBuildIndex: () => ({
buildIndex: async (args) => { buildIndex: async (args) => {
serviceEvents.push('build'); serviceEvents.push('build');
@@ -694,17 +633,12 @@ test('settings rebuild reopens the database from the configured Claude path', as
}, },
stop() { return Promise.resolve(); }, stop() { return Promise.resolve(); },
}), }),
}; },
} }],
if (request === 'chokidar') { ]);
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
const rebuild = ipcHandlers.get('settings:rebuildIndex'); const rebuild = ipcHandlers.get('settings:rebuildIndex');
assert.equal(typeof rebuild, 'function'); assert.equal(typeof rebuild, 'function');
@@ -722,8 +656,7 @@ test('settings rebuild reopens the database from the configured Claude path', as
); );
assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop')); assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop'));
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
@@ -749,9 +682,6 @@ test('settings rebuild keeps the existing database after a worker failure', asyn
const openedDbPaths = []; const openedDbPaths = [];
const closedDbPaths = []; const closedDbPaths = [];
const serviceEvents = []; const serviceEvents = [];
const originalLoad = Module._load;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeDatabase { class FakeDatabase {
constructor(dbPath) { constructor(dbPath) {
@@ -767,57 +697,41 @@ test('settings rebuild keeps the existing database after a worker failure', asyn
class FakeBrowserWindow { class FakeBrowserWindow {
constructor() { constructor() {
this.webContents = { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} };
on() {},
setZoomLevel() {},
openDevTools() {},
send() {},
};
} }
loadFile() {} loadFile() {}
loadURL() {} loadURL() {}
close() {} close() {}
static getAllWindows() { static getAllWindows() { return []; }
return []; static fromWebContents() { return null; }
}
static fromWebContents() {
return null;
}
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, {
return { namedExports: electronNamespace({
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow, BrowserWindow: FakeBrowserWindow,
ipcMain: { ipcMain: {
handle(channel, handler) { handle(channel, handler) {
ipcHandlers.set(channel, handler); ipcHandlers.set(channel, handler);
}, },
}, },
clipboard: {}, }),
dialog: {}, }],
nativeImage: {}, [DATABASE_URL, { defaultExport: FakeDatabase }],
}; [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
} [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
if (request === 'better-sqlite3') return FakeDatabase; [INDEXER_SERVICE_URL, {
if (request === './indexer') return { writeHeartbeat() {} }; namedExports: {
if (request === './indexer-service') {
return {
createIndexerService: () => ({ createIndexerService: () => ({
start() { serviceEvents.push('start'); }, start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); }, stop() { serviceEvents.push('stop'); },
idle: async () => { serviceEvents.push('idle'); }, idle: async () => { serviceEvents.push('idle'); },
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}), }),
}; },
} }],
if (request === './indexer-worker-client') { [INDEXER_WORKER_URL, {
return { namedExports: {
createWorkerBuildIndex: () => ({ createWorkerBuildIndex: () => ({
buildIndex: async () => { buildIndex: async () => {
serviceEvents.push('build'); serviceEvents.push('build');
@@ -825,17 +739,12 @@ test('settings rebuild keeps the existing database after a worker failure', asyn
}, },
stop() {}, stop() {},
}), }),
}; },
} }],
if (request === 'chokidar') { ]);
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
const rebuild = ipcHandlers.get('settings:rebuildIndex'); const rebuild = ipcHandlers.get('settings:rebuildIndex');
const openCountBeforeRebuild = openedDbPaths.length; const openCountBeforeRebuild = openedDbPaths.length;
@@ -852,8 +761,7 @@ test('settings rebuild keeps the existing database after a worker failure', asyn
assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop')); assert.ok(serviceEvents.indexOf('build') > serviceEvents.indexOf('stop'));
assert.ok(serviceEvents.lastIndexOf('start') > serviceEvents.indexOf('build')); assert.ok(serviceEvents.lastIndexOf('start') > serviceEvents.indexOf('build'));
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
@@ -877,9 +785,7 @@ test('settings rebuild cancels an in-flight background build instead of waiting
const ipcHandlers = new Map(); const ipcHandlers = new Map();
const serviceEvents = []; const serviceEvents = [];
const originalLoad = Module._load; let buildIndexCalls = 0;
const mainPath = require.resolve('../app/src/main/index.js');
delete require.cache[mainPath];
class FakeDatabase { class FakeDatabase {
pragma() {} pragma() {}
@@ -891,58 +797,41 @@ test('settings rebuild cancels an in-flight background build instead of waiting
class FakeBrowserWindow { class FakeBrowserWindow {
constructor() { constructor() {
this.webContents = { this.webContents = { on() {}, setZoomLevel() {}, openDevTools() {}, send() {} };
on() {},
setZoomLevel() {},
openDevTools() {},
send() {},
};
} }
loadFile() {} loadFile() {}
loadURL() {} loadURL() {}
close() {} close() {}
static getAllWindows() { static getAllWindows() { return []; }
return []; static fromWebContents() { return null; }
}
static fromWebContents() {
return null;
}
} }
Module._load = function patchedLoad(request, parent, isMain) { const restore = registerMocks([
if (request === 'electron') { [ELECTRON_URL, {
return { namedExports: electronNamespace({
app: {
whenReady: () => Promise.resolve(),
on() {},
quit() {},
},
BrowserWindow: FakeBrowserWindow, BrowserWindow: FakeBrowserWindow,
ipcMain: { ipcMain: {
handle(channel, handler) { handle(channel, handler) {
ipcHandlers.set(channel, handler); ipcHandlers.set(channel, handler);
}, },
}, },
clipboard: {}, }),
dialog: {}, }],
nativeImage: {}, [DATABASE_URL, { defaultExport: FakeDatabase }],
}; [CHOKIDAR_URL, { defaultExport: noopChokidar() }],
} [INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
if (request === 'better-sqlite3') return FakeDatabase; [INDEXER_SERVICE_URL, {
if (request === './indexer') return { writeHeartbeat() {} }; namedExports: {
if (request === './indexer-service') {
return {
createIndexerService: () => ({ createIndexerService: () => ({
start() { serviceEvents.push('start'); }, start() { serviceEvents.push('start'); },
stop() { serviceEvents.push('stop'); }, stop() { serviceEvents.push('stop'); },
idle: async () => new Promise(() => {}), idle: async () => new Promise(() => {}),
runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); }, runBuildNow() { serviceEvents.push('runBuildNow'); return Promise.resolve(); },
}), }),
}; },
} }],
if (request === './indexer-worker-client') { [INDEXER_WORKER_URL, {
let buildIndexCalls = 0; namedExports: {
return {
createWorkerBuildIndex: () => ({ createWorkerBuildIndex: () => ({
buildIndex: async (args) => { buildIndex: async (args) => {
serviceEvents.push(`build-${++buildIndexCalls}`); serviceEvents.push(`build-${++buildIndexCalls}`);
@@ -954,17 +843,12 @@ test('settings rebuild cancels an in-flight background build instead of waiting
return Promise.resolve(); return Promise.resolve();
}, },
}), }),
}; },
} }],
if (request === 'chokidar') { ]);
return { watch: () => ({ on() { return this; }, close() {} }) };
}
return originalLoad.call(this, request, parent, isMain);
};
try { try {
require('../app/src/main/index.js'); await importMain();
await new Promise(resolve => setImmediate(resolve));
const rebuild = ipcHandlers.get('settings:rebuildIndex'); const rebuild = ipcHandlers.get('settings:rebuildIndex');
const outcome = await Promise.race([ const outcome = await Promise.race([
@@ -976,8 +860,7 @@ test('settings rebuild cancels an in-flight background build instead of waiting
assert.ok(serviceEvents.indexOf('worker-stop') > serviceEvents.indexOf('stop')); assert.ok(serviceEvents.indexOf('worker-stop') > serviceEvents.indexOf('stop'));
assert.ok(serviceEvents.some(event => event.startsWith('build-'))); assert.ok(serviceEvents.some(event => event.startsWith('build-')));
} finally { } finally {
Module._load = originalLoad; restore();
delete require.cache[mainPath];
process.env.HOME = originalHome; process.env.HOME = originalHome;
rmSync(home, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });
} }
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { buildRecapExportQuery, cleanRecapFilename } = require('../app/src/main/recap-capture-query.js'); import { buildRecapExportQuery, cleanRecapFilename } from '../app/src/main/recap-capture-query.js';
test('recap export query includes the selected recap filename', () => { test('recap export query includes the selected recap filename', () => {
const query = buildRecapExportQuery({ const query = buildRecapExportQuery({