refactor: remove root node and npm workflow deps

This commit is contained in:
2026-03-07 21:19:14 -08:00
parent f0418c6e56
commit d0c11d347b
32 changed files with 215 additions and 299 deletions

View File

@@ -1,4 +1,4 @@
import { ipcMain } from 'electron';
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -25,6 +25,8 @@ import {
} from '../../shared/ipc/validators';
import { buildJimakuSubtitleFilenameFromMediaPath } from './jimaku-download-path';
const { ipcMain } = electron;
const logger = createLogger('main:anki-jimaku-ipc');
export interface AnkiJimakuIpcDeps {

View File

@@ -3,9 +3,9 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { DatabaseSync as NodeDatabaseSync } from 'node:sqlite';
import { toMonthKey } from './immersion-tracker/maintenance';
import { enqueueWrite } from './immersion-tracker/queue';
import { Database, type DatabaseSync } from './immersion-tracker/sqlite';
import {
deriveCanonicalTitle,
normalizeText,
@@ -17,22 +17,6 @@ type ImmersionTrackerService = import('./immersion-tracker-service').ImmersionTr
type ImmersionTrackerServiceCtor =
typeof import('./immersion-tracker-service').ImmersionTrackerService;
type DatabaseSyncCtor = typeof NodeDatabaseSync;
const DatabaseSync: DatabaseSyncCtor | null = (() => {
try {
return (require('node:sqlite') as { DatabaseSync?: DatabaseSyncCtor }).DatabaseSync ?? null;
} catch {
return null;
}
})();
const testIfSqlite = DatabaseSync ? test : test.skip;
if (!DatabaseSync) {
console.warn(
'Skipping SQLite-backed immersion tracker persistence tests in this runtime; run `bun run test:immersion:sqlite` for real DB coverage.',
);
}
let trackerCtor: ImmersionTrackerServiceCtor | null = null;
async function loadTrackerCtor(): Promise<ImmersionTrackerServiceCtor> {
@@ -89,7 +73,7 @@ test('seam: toMonthKey uses UTC calendar month', () => {
assert.equal(toMonthKey(Date.UTC(2026, 1, 1, 0, 0, 0, 0)), 202602);
});
testIfSqlite('startSession generates UUID-like session identifiers', async () => {
test('startSession generates UUID-like session identifiers', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
@@ -105,7 +89,7 @@ testIfSqlite('startSession generates UUID-like session identifiers', async () =>
privateApi.flushTelemetry(true);
privateApi.flushNow();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
const row = db.prepare('SELECT session_uuid FROM imm_sessions LIMIT 1').get() as {
session_uuid: string;
} | null;
@@ -120,7 +104,7 @@ testIfSqlite('startSession generates UUID-like session identifiers', async () =>
}
});
testIfSqlite('destroy finalizes active session and persists final telemetry', async () => {
test('destroy finalizes active session and persists final telemetry', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
@@ -132,7 +116,7 @@ testIfSqlite('destroy finalizes active session and persists final telemetry', as
tracker.recordSubtitleLine('Hello immersion', 0, 1);
tracker.destroy();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
const sessionRow = db.prepare('SELECT ended_at_ms FROM imm_sessions LIMIT 1').get() as {
ended_at_ms: number | null;
} | null;
@@ -150,7 +134,7 @@ testIfSqlite('destroy finalizes active session and persists final telemetry', as
}
});
testIfSqlite('persists and retrieves minimum immersion tracking fields', async () => {
test('persists and retrieves minimum immersion tracking fields', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
@@ -178,7 +162,7 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
tracker.destroy();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
const videoRow = db
.prepare('SELECT canonical_title, source_path, duration_ms FROM imm_videos LIMIT 1')
.get() as {
@@ -190,7 +174,7 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
.prepare(
`SELECT lines_seen, words_seen, tokens_seen, cards_mined
FROM imm_session_telemetry
ORDER BY sample_ms DESC
ORDER BY sample_ms DESC, telemetry_id DESC
LIMIT 1`,
)
.get() as {
@@ -217,7 +201,7 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
}
});
testIfSqlite('applies configurable queue, flush, and retention policy', async () => {
test('applies configurable queue, flush, and retention policy', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
@@ -270,7 +254,7 @@ testIfSqlite('applies configurable queue, flush, and retention policy', async ()
}
});
testIfSqlite('monthly rollups are grouped by calendar month', async () => {
test('monthly rollups are grouped by calendar month', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
@@ -278,7 +262,7 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => {
const Ctor = await loadTrackerCtor();
tracker = new Ctor({ dbPath });
const privateApi = tracker as unknown as {
db: NodeDatabaseSync;
db: DatabaseSync;
runRollupMaintenance: () => void;
};
@@ -433,16 +417,16 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => {
}
});
testIfSqlite('flushSingle reuses cached prepared statements', async () => {
test('flushSingle reuses cached prepared statements', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
let originalPrepare: NodeDatabaseSync['prepare'] | null = null;
let originalPrepare: DatabaseSync['prepare'] | null = null;
try {
const Ctor = await loadTrackerCtor();
tracker = new Ctor({ dbPath });
const privateApi = tracker as unknown as {
db: NodeDatabaseSync;
db: DatabaseSync;
flushSingle: (write: {
kind: 'telemetry' | 'event';
sessionId: number;
@@ -472,7 +456,7 @@ testIfSqlite('flushSingle reuses cached prepared statements', async () => {
originalPrepare = privateApi.db.prepare;
let prepareCalls = 0;
privateApi.db.prepare = (...args: Parameters<NodeDatabaseSync['prepare']>) => {
privateApi.db.prepare = (...args: Parameters<DatabaseSync['prepare']>) => {
prepareCalls += 1;
return originalPrepare!.apply(privateApi.db, args);
};
@@ -557,7 +541,7 @@ testIfSqlite('flushSingle reuses cached prepared statements', async () => {
assert.equal(prepareCalls, 0);
} finally {
if (tracker && originalPrepare) {
const privateApi = tracker as unknown as { db: NodeDatabaseSync };
const privateApi = tracker as unknown as { db: DatabaseSync };
privateApi.db.prepare = originalPrepare;
}
tracker?.destroy();

View File

@@ -1,9 +1,9 @@
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import * as fs from 'node:fs';
import { createLogger } from '../../logger';
import { getLocalVideoMetadata } from './immersion-tracker/metadata';
import { pruneRetention, runRollupMaintenance } from './immersion-tracker/maintenance';
import { Database, type DatabaseSync } from './immersion-tracker/sqlite';
import { finalizeSessionRecord, startSessionRecord } from './immersion-tracker/session';
import {
applyPragmas,
@@ -164,7 +164,7 @@ export class ImmersionTrackerService {
1,
3650,
) * 86_400_000;
this.db = new DatabaseSync(this.dbPath);
this.db = new Database(this.dbPath);
applyPragmas(this.db);
ensureSchema(this.db);
this.preparedStatements = createTrackerPreparedStatements(this.db);

View File

@@ -1,4 +1,4 @@
import type { DatabaseSync } from 'node:sqlite';
import type { DatabaseSync } from './sqlite';
const ROLLUP_STATE_KEY = 'last_rollup_sample_ms';
const DAILY_MS = 86_400_000;

View File

@@ -1,4 +1,4 @@
import type { DatabaseSync } from 'node:sqlite';
import type { DatabaseSync } from './sqlite';
import type {
ImmersionSessionRollupRow,
SessionSummaryQueryRow,
@@ -44,7 +44,7 @@ export function getSessionTimeline(
cards_mined AS cardsMined
FROM imm_session_telemetry
WHERE session_id = ?
ORDER BY sample_ms DESC
ORDER BY sample_ms DESC, telemetry_id DESC
LIMIT ?
`);
return prepared.all(sessionId, limit) as unknown as SessionTimelineRow[];
@@ -56,8 +56,8 @@ export function getQueryHints(db: DatabaseSync): {
} {
const sessions = db.prepare('SELECT COUNT(*) AS total FROM imm_sessions');
const active = db.prepare('SELECT COUNT(*) AS total FROM imm_sessions WHERE ended_at_ms IS NULL');
const totalSessions = Number(sessions.get()?.total ?? 0);
const activeSessions = Number(active.get()?.total ?? 0);
const totalSessions = Number((sessions.get() as { total?: number } | null)?.total ?? 0);
const activeSessions = Number((active.get() as { total?: number } | null)?.total ?? 0);
return { totalSessions, activeSessions };
}

View File

@@ -1,5 +1,5 @@
import crypto from 'node:crypto';
import type { DatabaseSync } from 'node:sqlite';
import type { DatabaseSync } from './sqlite';
import { createInitialSessionState } from './reducer';
import { SESSION_STATUS_ACTIVE, SESSION_STATUS_ENDED } from './types';
import type { SessionState } from './types';

View File

@@ -0,0 +1,20 @@
import Database = require('libsql');
export { Database };
export interface DatabaseRunResult {
changes: number;
lastInsertRowid: number | bigint;
}
export interface DatabaseStatement {
run(...params: unknown[]): DatabaseRunResult;
get(...params: unknown[]): unknown;
all(...params: unknown[]): unknown[];
}
export interface DatabaseSync {
prepare(source: string): DatabaseStatement;
exec(source: string): DatabaseSync;
close(): DatabaseSync;
}

View File

@@ -3,7 +3,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import type { DatabaseSync as NodeDatabaseSync } from 'node:sqlite';
import { Database } from './sqlite';
import { finalizeSessionRecord, startSessionRecord } from './session';
import {
createTrackerPreparedStatements,
@@ -13,22 +13,6 @@ import {
} from './storage';
import { EVENT_SUBTITLE_LINE, SESSION_STATUS_ENDED, SOURCE_TYPE_LOCAL } from './types';
type DatabaseSyncCtor = typeof NodeDatabaseSync;
const DatabaseSync: DatabaseSyncCtor | null = (() => {
try {
return (require('node:sqlite') as { DatabaseSync?: DatabaseSyncCtor }).DatabaseSync ?? null;
} catch {
return null;
}
})();
const testIfSqlite = DatabaseSync ? test : test.skip;
if (!DatabaseSync) {
console.warn(
'Skipping SQLite-backed immersion tracker storage/session tests in this runtime; run `bun run test:immersion:sqlite` for real DB coverage.',
);
}
function makeDbPath(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-imm-storage-session-'));
return path.join(dir, 'immersion.sqlite');
@@ -41,9 +25,9 @@ function cleanupDbPath(dbPath: string): void {
}
}
testIfSqlite('ensureSchema creates immersion core tables', () => {
test('ensureSchema creates immersion core tables', () => {
const dbPath = makeDbPath();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
try {
ensureSchema(db);
@@ -77,9 +61,9 @@ testIfSqlite('ensureSchema creates immersion core tables', () => {
}
});
testIfSqlite('start/finalize session updates ended_at and status', () => {
test('start/finalize session updates ended_at and status', () => {
const dbPath = makeDbPath();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
try {
ensureSchema(db);
@@ -111,9 +95,9 @@ testIfSqlite('start/finalize session updates ended_at and status', () => {
}
});
testIfSqlite('executeQueuedWrite inserts event and telemetry rows', () => {
test('executeQueuedWrite inserts event and telemetry rows', () => {
const dbPath = makeDbPath();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
try {
ensureSchema(db);
@@ -178,9 +162,9 @@ testIfSqlite('executeQueuedWrite inserts event and telemetry rows', () => {
}
});
testIfSqlite('executeQueuedWrite inserts and upserts word and kanji rows', () => {
test('executeQueuedWrite inserts and upserts word and kanji rows', () => {
const dbPath = makeDbPath();
const db = new DatabaseSync!(dbPath);
const db = new Database(dbPath);
try {
ensureSchema(db);

View File

@@ -1,4 +1,4 @@
import type { DatabaseSync } from 'node:sqlite';
import type { DatabaseSync } from './sqlite';
import { SCHEMA_VERSION } from './types';
import type { QueuedWrite, VideoMetadata } from './types';
@@ -13,7 +13,7 @@ function hasColumn(db: DatabaseSync, tableName: string, columnName: string): boo
return db
.prepare(`PRAGMA table_info(${tableName})`)
.all()
.some((row) => (row as { name: string }).name === columnName);
.some((row: unknown) => (row as { name: string }).name === columnName);
}
function addColumnIfMissing(db: DatabaseSync, tableName: string, columnName: string): void {

View File

@@ -1,4 +1,5 @@
import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
import electron from 'electron';
import type { IpcMainEvent } from 'electron';
import type {
RuntimeOptionId,
RuntimeOptionValue,
@@ -18,6 +19,8 @@ import {
parseSubsyncManualRunRequest,
} from '../../shared/ipc/validators';
const { BrowserWindow, ipcMain } = electron;
export interface IpcServiceDeps {
onOverlayModalClosed: (modal: OverlayHostedModal) => void;
onOverlayModalOpened?: (modal: OverlayHostedModal) => void;

View File

@@ -1,6 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { safeStorage } from 'electron';
import electron from 'electron';
const { safeStorage } = electron;
interface PersistedSessionPayload {
encryptedSession?: string;

View File

@@ -1,6 +1,6 @@
import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../types';
import { createFieldGroupingCallback } from './field-grouping';
import { BrowserWindow } from 'electron';
import type { BrowserWindow } from 'electron';
export function sendToVisibleOverlayRuntime<T extends string>(options: {
mainWindow: BrowserWindow | null;

View File

@@ -1,6 +1,7 @@
import { BrowserWindow } from 'electron';
import type { BrowserWindow } from 'electron';
import { BaseWindowTracker, createWindowTracker } from '../../window-trackers';
import {
AiConfig,
AnkiConnectConfig,
KikuFieldGroupingChoice,
KikuFieldGroupingRequestData,
@@ -13,6 +14,7 @@ type AnkiIntegrationLike = {
type CreateAnkiIntegrationArgs = {
config: AnkiConnectConfig;
aiConfig: AiConfig;
subtitleTimingTracker: unknown;
mpvClient: { send?: (payload: { command: string[] }) => void };
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
@@ -39,6 +41,7 @@ function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiInte
args.showDesktopNotification,
args.createFieldGroupingCallback(),
args.knownWordCacheStatePath,
args.aiConfig,
);
}
@@ -57,7 +60,7 @@ export function initializeOverlayRuntime(options: {
targetMpvSocketPath?: string | null,
) => BaseWindowTracker | null;
getMpvSocketPath: () => string;
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
getSubtitleTimingTracker: () => unknown | null;
getMpvClient: () => {
send?: (payload: { command: string[] }) => void;
@@ -118,6 +121,7 @@ export function initializeOverlayRuntime(options: {
const createAnkiIntegration = options.createAnkiIntegration ?? createDefaultAnkiIntegration;
const integration = createAnkiIntegration({
config: effectiveAnkiConfig,
aiConfig: config.ai ?? {},
subtitleTimingTracker,
mpvClient,
showDesktopNotification: options.showDesktopNotification,

View File

@@ -1,8 +1,9 @@
import { globalShortcut } from 'electron';
import electron from 'electron';
import { ConfiguredShortcuts } from '../utils/shortcut-config';
import { isGlobalShortcutRegisteredSafe } from './shortcut-fallback';
import { createLogger } from '../../logger';
const { globalShortcut } = electron;
const logger = createLogger('main:overlay-shortcut-service');
export interface OverlayShortcutHandlers {

View File

@@ -1,9 +1,11 @@
import { BrowserWindow } from 'electron';
import electron from 'electron';
import type { BrowserWindow } from 'electron';
import * as path from 'path';
import { WindowGeometry } from '../../types';
import { createLogger } from '../../logger';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
const { BrowserWindow: ElectronBrowserWindow } = electron;
const logger = createLogger('main:overlay-window');
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
@@ -18,7 +20,7 @@ function loadOverlayWindowLayer(window: BrowserWindow, layer: OverlayWindowKind)
.loadFile(htmlPath, {
query: { layer },
})
.catch((err) => {
.catch((err: unknown) => {
logger.error('Failed to load HTML file:', err);
});
}
@@ -90,7 +92,7 @@ export function createOverlayWindow(
onWindowClosed: (kind: OverlayWindowKind) => void;
},
): BrowserWindow {
const window = new BrowserWindow({
const window = new ElectronBrowserWindow({
show: false,
width: 800,
height: 600,

View File

@@ -1,4 +1,6 @@
import { globalShortcut } from 'electron';
import electron from 'electron';
const { globalShortcut } = electron;
export function isGlobalShortcutRegisteredSafe(accelerator: string): boolean {
try {

View File

@@ -1,6 +1,8 @@
import { BrowserWindow, globalShortcut } from 'electron';
import electron from 'electron';
import type { BrowserWindow } from 'electron';
import { createLogger } from '../../logger';
const { globalShortcut } = electron;
const logger = createLogger('main:shortcut');
export interface GlobalShortcutConfig {

View File

@@ -1,4 +1,5 @@
import { BrowserWindow, Extension, session } from 'electron';
import electron from 'electron';
import type { BrowserWindow, Extension } from 'electron';
import * as fs from 'fs';
import { createLogger } from '../../logger';
import { ensureExtensionCopy } from './yomitan-extension-copy';
@@ -7,6 +8,7 @@ import {
resolveExistingYomitanExtensionPath,
} from './yomitan-extension-paths';
const { session } = electron;
const logger = createLogger('main:yomitan-extension-loader');
export interface YomitanExtensionLoaderDeps {

View File

@@ -1,6 +1,8 @@
import { BrowserWindow, Extension, session } from 'electron';
import electron from 'electron';
import type { BrowserWindow, Extension } from 'electron';
import { createLogger } from '../../logger';
const { BrowserWindow: ElectronBrowserWindow, session } = electron;
const logger = createLogger('main:yomitan-settings');
export interface OpenYomitanSettingsWindowOptions {
@@ -28,7 +30,7 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
logger.info('Creating new settings window for extension:', options.yomitanExt.id);
const settingsWindow = new BrowserWindow({
const settingsWindow = new ElectronBrowserWindow({
width: 1200,
height: 800,
show: false,

View File

@@ -1,7 +1,8 @@
import { Notification, nativeImage } from 'electron';
import electron from 'electron';
import * as fs from 'fs';
import { createLogger } from '../../logger';
const { Notification, nativeImage } = electron;
const logger = createLogger('core:notification');
export function showDesktopNotification(

View File

@@ -2264,6 +2264,9 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
failHandlers: {
logError: (details) => logger.error(details),
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
setExitCode: (code) => {
process.exitCode = code;
},
quit: () => app.quit(),
},
},
@@ -2272,6 +2275,9 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
failHandlers: {
logError: (message) => logger.error(message),
showErrorBox: (title, message) => dialog.showErrorBox(title, message),
setExitCode: (code) => {
process.exitCode = code;
},
quit: () => app.quit(),
},
},

View File

@@ -32,10 +32,10 @@ test('buildConfigWarningSummary includes warnings with formatted values', () =>
test('buildConfigWarningNotificationBody includes concise warning details', () => {
const body = buildConfigWarningNotificationBody('/tmp/config.jsonc', [
{
path: 'ankiConnect.openRouter',
message: 'Deprecated key; use ankiConnect.ai instead.',
path: 'ankiConnect.ai',
message: 'Expected boolean.',
value: { enabled: true },
fallback: {},
fallback: false,
},
{
path: 'ankiConnect.isLapis.sentenceCardSentenceField',
@@ -47,7 +47,7 @@ test('buildConfigWarningNotificationBody includes concise warning details', () =
assert.match(body, /2 config validation issue\(s\) detected\./);
assert.match(body, /File: \/tmp\/config\.jsonc/);
assert.match(body, /1\. ankiConnect\.openRouter: Deprecated key; use ankiConnect\.ai instead\./);
assert.match(body, /1\. ankiConnect\.ai: Expected boolean\./);
assert.match(
body,
/2\. ankiConnect\.isLapis\.sentenceCardSentenceField: Deprecated key; sentence-card sentence field is fixed to Sentence\./,
@@ -81,8 +81,7 @@ test('buildConfigParseErrorDetails includes path error and restart guidance', ()
test('failStartupFromConfig invokes handlers and throws', () => {
const calls: string[] = [];
const previousExitCode = process.exitCode;
process.exitCode = 0;
const exitCodes: number[] = [];
assert.throws(
() =>
@@ -93,6 +92,9 @@ test('failStartupFromConfig invokes handlers and throws', () => {
showErrorBox: (title, details) => {
calls.push(`dialog:${title}:${details}`);
},
setExitCode: (code) => {
exitCodes.push(code);
},
quit: () => {
calls.push('quit');
},
@@ -100,8 +102,6 @@ test('failStartupFromConfig invokes handlers and throws', () => {
/bad value/,
);
assert.equal(process.exitCode, 1);
assert.deepEqual(exitCodes, [1]);
assert.deepEqual(calls, ['log:bad value', 'dialog:Config Error:bad value', 'quit']);
process.exitCode = previousExitCode;
});

View File

@@ -3,6 +3,7 @@ import type { ConfigValidationWarning } from '../types';
export type StartupFailureHandlers = {
logError: (details: string) => void;
showErrorBox: (title: string, details: string) => void;
setExitCode?: (code: number) => void;
quit: () => void;
};
@@ -98,7 +99,10 @@ export function failStartupFromConfig(
): never {
handlers.logError(details);
handlers.showErrorBox(title, details);
process.exitCode = 1;
handlers.setExitCode?.(1);
if (!handlers.setExitCode) {
process.exitCode = 1;
}
handlers.quit();
throw new Error(details);
}

View File

@@ -1,22 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
async function loadRegistryOrSkip(t: test.TestContext) {
try {
return await import('./registry');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('node:sqlite')) {
t.skip('registry import requires node:sqlite support in this runtime');
return null;
}
throw error;
}
async function loadRegistry() {
return import('./registry');
}
test('createMainRuntimeRegistry exposes expected runtime domains', async (t) => {
const loaded = await loadRegistryOrSkip(t);
if (!loaded) return;
test('createMainRuntimeRegistry exposes expected runtime domains', async () => {
const loaded = await loadRegistry();
const { createMainRuntimeRegistry } = loaded;
const registry = createMainRuntimeRegistry();
@@ -30,9 +20,8 @@ test('createMainRuntimeRegistry exposes expected runtime domains', async (t) =>
assert.ok(registry.mining);
});
test('registry domains expose representative factories', async (t) => {
const loaded = await loadRegistryOrSkip(t);
if (!loaded) return;
test('registry domains expose representative factories', async () => {
const loaded = await loadRegistry();
const { createMainRuntimeRegistry } = loaded;
const registry = createMainRuntimeRegistry();

View File

@@ -17,6 +17,9 @@ export function createBuildReloadConfigMainDepsHandler(deps: ReloadConfigMainDep
logError: (details: string) => deps.failHandlers.logError(details),
showErrorBox: (title: string, details: string) =>
deps.failHandlers.showErrorBox(title, details),
setExitCode: deps.failHandlers.setExitCode
? (code: number) => deps.failHandlers.setExitCode?.(code)
: undefined,
quit: () => deps.failHandlers.quit(),
},
});
@@ -29,6 +32,9 @@ export function createBuildCriticalConfigErrorMainDepsHandler(deps: CriticalConf
logError: (details: string) => deps.failHandlers.logError(details),
showErrorBox: (title: string, details: string) =>
deps.failHandlers.showErrorBox(title, details),
setExitCode: deps.failHandlers.setExitCode
? (code: number) => deps.failHandlers.setExitCode?.(code)
: undefined,
quit: () => deps.failHandlers.quit(),
},
});

View File

@@ -55,8 +55,7 @@ test('createReloadConfigHandler runs success flow with warnings', async () => {
test('createReloadConfigHandler fails startup for parse errors', () => {
const calls: string[] = [];
const previousExitCode = process.exitCode;
process.exitCode = 0;
const exitCodes: number[] = [];
const reloadConfig = createReloadConfigHandler({
reloadConfigStrict: () => ({
@@ -74,12 +73,13 @@ test('createReloadConfigHandler fails startup for parse errors', () => {
failHandlers: {
logError: (details) => calls.push(`error:${details}`),
showErrorBox: (title, details) => calls.push(`dialog:${title}:${details}`),
setExitCode: (code) => exitCodes.push(code),
quit: () => calls.push('quit'),
},
});
assert.throws(() => reloadConfig(), /Failed to parse config file at:/);
assert.equal(process.exitCode, 1);
assert.deepEqual(exitCodes, [1]);
assert.ok(calls.some((entry) => entry.startsWith('error:Failed to parse config file at:')));
assert.ok(calls.some((entry) => entry.includes('/tmp/config.jsonc')));
assert.ok(calls.some((entry) => entry.includes('Error: unexpected token')));
@@ -91,20 +91,18 @@ test('createReloadConfigHandler fails startup for parse errors', () => {
);
assert.ok(calls.includes('quit'));
assert.equal(calls.includes('hotReload:start'), false);
process.exitCode = previousExitCode;
});
test('createCriticalConfigErrorHandler formats and fails', () => {
const calls: string[] = [];
const previousExitCode = process.exitCode;
process.exitCode = 0;
const exitCodes: number[] = [];
const handleCriticalErrors = createCriticalConfigErrorHandler({
getConfigPath: () => '/tmp/config.jsonc',
failHandlers: {
logError: (details) => calls.push(`error:${details}`),
showErrorBox: (title, details) => calls.push(`dialog:${title}:${details}`),
setExitCode: (code) => exitCodes.push(code),
quit: () => calls.push('quit'),
},
});
@@ -114,11 +112,9 @@ test('createCriticalConfigErrorHandler formats and fails', () => {
/Critical config validation failed/,
);
assert.equal(process.exitCode, 1);
assert.deepEqual(exitCodes, [1]);
assert.ok(calls.some((entry) => entry.includes('/tmp/config.jsonc')));
assert.ok(calls.some((entry) => entry.includes('1. foo invalid')));
assert.ok(calls.some((entry) => entry.includes('2. bar invalid')));
assert.ok(calls.includes('quit'));
process.exitCode = previousExitCode;
});

View File

@@ -31,6 +31,7 @@ export type ReloadConfigRuntimeDeps = {
failHandlers: {
logError: (details: string) => void;
showErrorBox: (title: string, details: string) => void;
setExitCode?: (code: number) => void;
quit: () => void;
};
};
@@ -40,6 +41,7 @@ export type CriticalConfigErrorRuntimeDeps = {
failHandlers: {
logError: (details: string) => void;
showErrorBox: (title: string, details: string) => void;
setExitCode?: (code: number) => void;
quit: () => void;
};
};