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:
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+259
-376
@@ -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: {},
|
[DATABASE_URL, { defaultExport: FakeDatabase }],
|
||||||
};
|
[CHOKIDAR_URL, { defaultExport: noopChokidar() }],
|
||||||
}
|
[INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
|
||||||
if (request === 'better-sqlite3') return FakeDatabase;
|
[INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }],
|
||||||
if (request === './indexer') return { writeHeartbeat() {} };
|
[INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
|
||||||
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));
|
|
||||||
|
|
||||||
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,8 +368,31 @@ 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];
|
// 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 {
|
class FakeBrowserWindow {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -387,75 +405,26 @@ test('main process migrates an existing app database before source-filtered IPC
|
|||||||
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: SqliteCompatDatabase }],
|
||||||
};
|
[CHOKIDAR_URL, { defaultExport: noopChokidar() }],
|
||||||
}
|
[INDEXER_URL, { namedExports: { writeHeartbeat() {} } }],
|
||||||
if (request === 'better-sqlite3') {
|
[INDEXER_SERVICE_URL, { namedExports: defaultIndexerService() }],
|
||||||
return class SqliteCompatDatabase {
|
[INDEXER_WORKER_URL, { namedExports: defaultIndexerWorkerClient() }],
|
||||||
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 {
|
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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user