mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
fix(config): improve startup validation and config error reporting
This commit is contained in:
@@ -149,3 +149,92 @@ test('runAppReadyRuntime does not await background warmups', async () => {
|
||||
assert.ok(releaseWarmup);
|
||||
releaseWarmup();
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime exits before service init when critical anki mappings are invalid', async () => {
|
||||
const capturedErrors: string[][] = [];
|
||||
const { deps, calls } = makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: 'auto' },
|
||||
secondarySub: {},
|
||||
ankiConnect: {
|
||||
enabled: true,
|
||||
fields: {
|
||||
audio: 'ExpressionAudio',
|
||||
image: 'Picture',
|
||||
sentence: ' ',
|
||||
miscInfo: 'MiscInfo',
|
||||
translation: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
onCriticalConfigErrors: (errors) => {
|
||||
capturedErrors.push(errors);
|
||||
},
|
||||
});
|
||||
|
||||
await runAppReadyRuntime(deps);
|
||||
|
||||
assert.equal(capturedErrors.length, 1);
|
||||
assert.deepEqual(capturedErrors[0], [
|
||||
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
||||
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
||||
]);
|
||||
assert.ok(calls.includes('reloadConfig'));
|
||||
assert.equal(calls.includes('createMpvClient'), false);
|
||||
assert.equal(calls.includes('initRuntimeOptionsManager'), false);
|
||||
assert.equal(calls.includes('startBackgroundWarmups'), false);
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime aggregates multiple critical anki mapping errors', async () => {
|
||||
const capturedErrors: string[][] = [];
|
||||
const { deps, calls } = makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: 'auto' },
|
||||
secondarySub: {},
|
||||
ankiConnect: {
|
||||
enabled: true,
|
||||
fields: {
|
||||
audio: ' ',
|
||||
image: '',
|
||||
sentence: '\t',
|
||||
miscInfo: ' ',
|
||||
translation: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
onCriticalConfigErrors: (errors) => {
|
||||
capturedErrors.push(errors);
|
||||
},
|
||||
});
|
||||
|
||||
await runAppReadyRuntime(deps);
|
||||
|
||||
assert.equal(capturedErrors.length, 1);
|
||||
assert.equal(capturedErrors[0].length, 5);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.equal(calls.includes('loadSubtitlePosition'), false);
|
||||
});
|
||||
|
||||
@@ -76,6 +76,16 @@ interface AppReadyConfigLike {
|
||||
secondarySub?: {
|
||||
defaultMode?: SecondarySubMode;
|
||||
};
|
||||
ankiConnect?: {
|
||||
enabled?: boolean;
|
||||
fields?: {
|
||||
audio?: string;
|
||||
image?: string;
|
||||
sentence?: string;
|
||||
miscInfo?: string;
|
||||
translation?: string;
|
||||
};
|
||||
};
|
||||
websocket?: {
|
||||
enabled?: boolean | 'auto';
|
||||
port?: number;
|
||||
@@ -113,9 +123,38 @@ export interface AppReadyRuntimeDeps {
|
||||
initializeOverlayRuntime: () => void;
|
||||
handleInitialArgs: () => void;
|
||||
logDebug?: (message: string) => void;
|
||||
onCriticalConfigErrors?: (errors: string[]) => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
const REQUIRED_ANKI_FIELD_MAPPING_KEYS = [
|
||||
'audio',
|
||||
'image',
|
||||
'sentence',
|
||||
'miscInfo',
|
||||
'translation',
|
||||
] as const;
|
||||
|
||||
function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] {
|
||||
if (!config.ankiConnect?.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const fields = config.ankiConnect.fields ?? {};
|
||||
|
||||
for (const key of REQUIRED_ANKI_FIELD_MAPPING_KEYS) {
|
||||
const value = fields[key];
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
errors.push(
|
||||
`ankiConnect.fields.${key} must be a non-empty string when ankiConnect is enabled.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibility(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
@@ -151,16 +190,25 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
const startupStartedAtMs = now();
|
||||
deps.logDebug?.('App-ready critical path started.');
|
||||
|
||||
deps.loadSubtitlePosition();
|
||||
deps.resolveKeybindings();
|
||||
deps.createMpvClient();
|
||||
|
||||
deps.reloadConfig();
|
||||
const config = deps.getResolvedConfig();
|
||||
const criticalConfigErrors = getStartupCriticalConfigErrors(config);
|
||||
if (criticalConfigErrors.length > 0) {
|
||||
deps.onCriticalConfigErrors?.(criticalConfigErrors);
|
||||
deps.logDebug?.(
|
||||
`App-ready critical path aborted after config validation in ${now() - startupStartedAtMs}ms.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
deps.setLogLevel(config.logging?.level ?? 'info', 'config');
|
||||
for (const warning of deps.getConfigWarnings()) {
|
||||
deps.logConfigWarning(warning);
|
||||
}
|
||||
|
||||
deps.loadSubtitlePosition();
|
||||
deps.resolveKeybindings();
|
||||
deps.createMpvClient();
|
||||
deps.initRuntimeOptionsManager();
|
||||
deps.setSecondarySubMode(config.secondarySub?.defaultMode ?? deps.defaultSecondarySubMode);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user