refactor(main): modularize runtime and harden anilist setup flow

This commit is contained in:
2026-02-19 16:04:59 -08:00
parent 58f28b7b55
commit 162be118e1
73 changed files with 4413 additions and 1251 deletions

View File

@@ -0,0 +1,119 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createCriticalConfigErrorHandler,
createReloadConfigHandler,
} from './startup-config';
test('createReloadConfigHandler runs success flow with warnings', async () => {
const calls: string[] = [];
const refreshCalls: { force: boolean }[] = [];
const reloadConfig = createReloadConfigHandler({
reloadConfigStrict: () => ({
ok: true,
path: '/tmp/config.jsonc',
warnings: [
{
path: 'ankiConnect.pollingRate',
message: 'must be >= 50',
value: 10,
fallback: 250,
},
],
}),
logInfo: (message) => calls.push(`info:${message}`),
logWarning: (message) => calls.push(`warn:${message}`),
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
startConfigHotReload: () => calls.push('hotReload:start'),
refreshAnilistClientSecretState: async (options) => {
refreshCalls.push(options);
},
failHandlers: {
logError: (details) => calls.push(`error:${details}`),
showErrorBox: (title, details) => calls.push(`dialog:${title}:${details}`),
quit: () => calls.push('quit'),
},
});
reloadConfig();
await Promise.resolve();
assert.ok(calls.some((entry) => entry.startsWith('info:Using config file: /tmp/config.jsonc')));
assert.ok(calls.some((entry) => entry.startsWith('warn:[config] Validation found 1 issue(s)')));
assert.ok(
calls.some((entry) =>
entry.includes('notify:SubMiner:1 config validation issue(s) detected.'),
),
);
assert.ok(calls.some((entry) => entry.includes('1. ankiConnect.pollingRate: must be >= 50')));
assert.ok(calls.includes('hotReload:start'));
assert.deepEqual(refreshCalls, [{ force: true }]);
});
test('createReloadConfigHandler fails startup for parse errors', () => {
const calls: string[] = [];
const previousExitCode = process.exitCode;
process.exitCode = 0;
const reloadConfig = createReloadConfigHandler({
reloadConfigStrict: () => ({
ok: false,
path: '/tmp/config.jsonc',
error: 'unexpected token',
}),
logInfo: (message) => calls.push(`info:${message}`),
logWarning: (message) => calls.push(`warn:${message}`),
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
startConfigHotReload: () => calls.push('hotReload:start'),
refreshAnilistClientSecretState: async () => {
calls.push('refresh');
},
failHandlers: {
logError: (details) => calls.push(`error:${details}`),
showErrorBox: (title, details) => calls.push(`dialog:${title}:${details}`),
quit: () => calls.push('quit'),
},
});
assert.throws(() => reloadConfig(), /Failed to parse config file at:/);
assert.equal(process.exitCode, 1);
assert.ok(calls.some((entry) => entry.startsWith('error:Failed to parse config file at:')));
assert.ok(
calls.some((entry) =>
entry.startsWith('dialog:SubMiner config parse error:Failed to parse config file at:'),
),
);
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 handleCriticalErrors = createCriticalConfigErrorHandler({
getConfigPath: () => '/tmp/config.jsonc',
failHandlers: {
logError: (details) => calls.push(`error:${details}`),
showErrorBox: (title, details) => calls.push(`dialog:${title}:${details}`),
quit: () => calls.push('quit'),
},
});
assert.throws(
() => handleCriticalErrors(['foo invalid', 'bar invalid']),
/Critical config validation failed/,
);
assert.equal(process.exitCode, 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;
});