mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
Switch known-word cache to incremental sync and doctor refresh
- Load persisted known-word cache on startup; reconcile adds/deletes/edits on timed sync - Add `knownWords.addMinedWordsImmediately` (default `true`) for immediate mined-word updates - Route full rebuild to explicit `subminer doctor --refresh-known-words` and expand tests/docs
This commit is contained in:
@@ -77,11 +77,37 @@ test('doctor command exits non-zero for missing hard dependencies', () => {
|
||||
commandExists: () => false,
|
||||
configExists: () => true,
|
||||
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
|
||||
runAppCommandWithInherit: () => {
|
||||
throw new Error('unexpected app handoff');
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('doctor command forwards refresh-known-words to app binary', () => {
|
||||
const context = createContext();
|
||||
context.args.doctor = true;
|
||||
context.args.doctorRefreshKnownWords = true;
|
||||
const forwarded: string[][] = [];
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
runDoctorCommand(context, {
|
||||
commandExists: () => false,
|
||||
configExists: () => true,
|
||||
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
throw new ExitSignal(0);
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 0,
|
||||
);
|
||||
|
||||
assert.deepEqual(forwarded, [['--refresh-known-words']]);
|
||||
});
|
||||
|
||||
test('mpv pre-app command exits non-zero when socket is not ready', async () => {
|
||||
const context = createContext();
|
||||
context.args.mpvStatus = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import { log } from '../log.js';
|
||||
import { runAppCommandWithInherit } from '../mpv.js';
|
||||
import { commandExists } from '../util.js';
|
||||
import { resolveMainConfigPath } from '../config-path.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
@@ -8,12 +9,14 @@ interface DoctorCommandDeps {
|
||||
commandExists(command: string): boolean;
|
||||
configExists(path: string): boolean;
|
||||
resolveMainConfigPath(): string;
|
||||
runAppCommandWithInherit(appPath: string, appArgs: string[]): never;
|
||||
}
|
||||
|
||||
const defaultDeps: DoctorCommandDeps = {
|
||||
commandExists,
|
||||
configExists: fs.existsSync,
|
||||
resolveMainConfigPath,
|
||||
runAppCommandWithInherit,
|
||||
};
|
||||
|
||||
export function runDoctorCommand(
|
||||
@@ -72,14 +75,21 @@ export function runDoctorCommand(
|
||||
},
|
||||
];
|
||||
|
||||
const hasHardFailure = checks.some((entry) =>
|
||||
entry.label === 'app binary' || entry.label === 'mpv' ? !entry.ok : false,
|
||||
);
|
||||
|
||||
for (const check of checks) {
|
||||
log(check.ok ? 'info' : 'warn', args.logLevel, `[doctor] ${check.label}: ${check.detail}`);
|
||||
}
|
||||
|
||||
if (args.doctorRefreshKnownWords) {
|
||||
if (!appPath) {
|
||||
processAdapter.exit(1);
|
||||
return true;
|
||||
}
|
||||
deps.runAppCommandWithInherit(appPath, ['--refresh-known-words']);
|
||||
}
|
||||
|
||||
const hasHardFailure = checks.some((entry) =>
|
||||
entry.label === 'app binary' || entry.label === 'mpv' ? !entry.ok : false,
|
||||
);
|
||||
processAdapter.exit(hasHardFailure ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
doctor: false,
|
||||
doctorRefreshKnownWords: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
@@ -206,6 +207,7 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
parsed.dictionaryTarget = parseDictionaryTarget(invocations.dictionaryTarget);
|
||||
}
|
||||
if (invocations.doctorTriggered) parsed.doctor = true;
|
||||
if (invocations.doctorRefreshKnownWords) parsed.doctorRefreshKnownWords = true;
|
||||
if (invocations.texthookerTriggered) parsed.texthookerOnly = true;
|
||||
|
||||
if (invocations.jellyfinInvocation) {
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CliInvocations {
|
||||
statsLogLevel: string | null;
|
||||
doctorTriggered: boolean;
|
||||
doctorLogLevel: string | null;
|
||||
doctorRefreshKnownWords: boolean;
|
||||
texthookerTriggered: boolean;
|
||||
texthookerLogLevel: string | null;
|
||||
}
|
||||
@@ -156,6 +157,7 @@ export function parseCliPrograms(
|
||||
let statsCleanupLifetime = false;
|
||||
let statsLogLevel: string | null = null;
|
||||
let doctorLogLevel: string | null = null;
|
||||
let doctorRefreshKnownWords = false;
|
||||
let texthookerLogLevel: string | null = null;
|
||||
let doctorTriggered = false;
|
||||
let texthookerTriggered = false;
|
||||
@@ -304,10 +306,12 @@ export function parseCliPrograms(
|
||||
commandProgram
|
||||
.command('doctor')
|
||||
.description('Run dependency and environment checks')
|
||||
.option('--refresh-known-words', 'Refresh known words cache')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((options: Record<string, unknown>) => {
|
||||
doctorTriggered = true;
|
||||
doctorLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
||||
doctorRefreshKnownWords = options.refreshKnownWords === true;
|
||||
});
|
||||
|
||||
commandProgram
|
||||
@@ -388,6 +392,7 @@ export function parseCliPrograms(
|
||||
statsLogLevel,
|
||||
doctorTriggered,
|
||||
doctorLogLevel,
|
||||
doctorRefreshKnownWords,
|
||||
texthookerTriggered,
|
||||
texthookerLogLevel,
|
||||
},
|
||||
|
||||
@@ -178,6 +178,33 @@ test('doctor reports checks and exits non-zero without hard dependencies', () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('doctor refresh-known-words forwards app refresh command without requiring mpv', () => {
|
||||
withTempDir((root) => {
|
||||
const homeDir = path.join(root, 'home');
|
||||
const xdgConfigHome = path.join(root, 'xdg');
|
||||
const appPath = path.join(root, 'fake-subminer.sh');
|
||||
const capturePath = path.join(root, 'captured-args.txt');
|
||||
fs.writeFileSync(
|
||||
appPath,
|
||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
||||
);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
|
||||
const env = {
|
||||
...makeTestEnv(homeDir, xdgConfigHome),
|
||||
PATH: '',
|
||||
Path: '',
|
||||
SUBMINER_APPIMAGE_PATH: appPath,
|
||||
SUBMINER_TEST_CAPTURE: capturePath,
|
||||
};
|
||||
const result = runLauncher(['doctor', '--refresh-known-words'], env);
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.equal(fs.readFileSync(capturePath, 'utf8'), '--refresh-known-words\n');
|
||||
assert.match(result.stdout, /\[doctor\] mpv: missing/);
|
||||
});
|
||||
});
|
||||
|
||||
test('youtube command rejects removed --mode option', () => {
|
||||
withTempDir((root) => {
|
||||
const homeDir = path.join(root, 'home');
|
||||
|
||||
@@ -40,6 +40,26 @@ test('runAppCommandCaptureOutput captures status and stdio', () => {
|
||||
assert.equal(result.error, undefined);
|
||||
});
|
||||
|
||||
test('runAppCommandCaptureOutput strips ELECTRON_RUN_AS_NODE from app child env', () => {
|
||||
const original = process.env.ELECTRON_RUN_AS_NODE;
|
||||
try {
|
||||
process.env.ELECTRON_RUN_AS_NODE = '1';
|
||||
const result = runAppCommandCaptureOutput(process.execPath, [
|
||||
'-e',
|
||||
'process.stdout.write(String(process.env.ELECTRON_RUN_AS_NODE ?? ""));',
|
||||
]);
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.equal(result.stdout, '');
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.ELECTRON_RUN_AS_NODE;
|
||||
} else {
|
||||
process.env.ELECTRON_RUN_AS_NODE = original;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('waitForUnixSocketReady returns false when socket never appears', async () => {
|
||||
const { dir, socketPath } = createTempSocketPath();
|
||||
try {
|
||||
@@ -137,6 +157,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
||||
dictionary: false,
|
||||
stats: false,
|
||||
doctor: false,
|
||||
doctorRefreshKnownWords: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
|
||||
@@ -661,7 +661,7 @@ export async function startOverlay(appPath: string, args: Args, socketPath: stri
|
||||
const target = resolveAppSpawnTarget(appPath, overlayArgs);
|
||||
state.overlayProc = spawn(target.command, target.args, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, SUBMINER_MPV_LOG: getMpvLogPath() },
|
||||
env: buildAppEnv(),
|
||||
});
|
||||
state.overlayManagedByLauncher = true;
|
||||
|
||||
@@ -688,7 +688,10 @@ export function launchTexthookerOnly(appPath: string, args: Args): never {
|
||||
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
|
||||
|
||||
log('info', args.logLevel, 'Launching texthooker mode...');
|
||||
const result = spawnSync(appPath, overlayArgs, { stdio: 'inherit' });
|
||||
const result = spawnSync(appPath, overlayArgs, {
|
||||
stdio: 'inherit',
|
||||
env: buildAppEnv(),
|
||||
});
|
||||
process.exit(result.status ?? 0);
|
||||
}
|
||||
|
||||
@@ -702,7 +705,10 @@ export function stopOverlay(args: Args): void {
|
||||
const stopArgs = ['--stop'];
|
||||
if (args.logLevel !== 'info') stopArgs.push('--log-level', args.logLevel);
|
||||
|
||||
spawnSync(state.appPath, stopArgs, { stdio: 'ignore' });
|
||||
spawnSync(state.appPath, stopArgs, {
|
||||
stdio: 'ignore',
|
||||
env: buildAppEnv(),
|
||||
});
|
||||
|
||||
if (state.overlayProc && !state.overlayProc.killed) {
|
||||
try {
|
||||
@@ -763,6 +769,7 @@ function buildAppEnv(): NodeJS.ProcessEnv {
|
||||
...process.env,
|
||||
SUBMINER_MPV_LOG: getMpvLogPath(),
|
||||
};
|
||||
delete env.ELECTRON_RUN_AS_NODE;
|
||||
const layers = env.VK_INSTANCE_LAYERS;
|
||||
if (typeof layers === 'string' && layers.trim().length > 0) {
|
||||
const filtered = layers
|
||||
|
||||
@@ -127,3 +127,10 @@ test('parseArgs maps stats rebuild action to cleanup lifetime mode', () => {
|
||||
assert.equal(parsed.statsCleanupVocab, false);
|
||||
assert.equal(parsed.statsCleanupLifetime, true);
|
||||
});
|
||||
|
||||
test('parseArgs maps doctor refresh-known-words flag', () => {
|
||||
const parsed = parseArgs(['doctor', '--refresh-known-words'], 'subminer', {});
|
||||
|
||||
assert.equal(parsed.doctor, true);
|
||||
assert.equal(parsed.doctorRefreshKnownWords, true);
|
||||
});
|
||||
|
||||
@@ -119,6 +119,7 @@ export interface Args {
|
||||
statsCleanupLifetime?: boolean;
|
||||
dictionaryTarget?: string;
|
||||
doctor: boolean;
|
||||
doctorRefreshKnownWords: boolean;
|
||||
configPath: boolean;
|
||||
configShow: boolean;
|
||||
mpvIdle: boolean;
|
||||
|
||||
Reference in New Issue
Block a user