mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor(core): normalize core service naming
Standardize core service module and export names to reduce naming ambiguity and make imports predictable across runtime, tests, scripts, and docs.
This commit is contained in:
318
src/core/services/cli-command.test.ts
Normal file
318
src/core/services/cli-command.test.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { CliCommandServiceDeps, handleCliCommand } from "./cli-command";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
toggleVisibleOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
settings: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
showVisibleOverlay: false,
|
||||
hideVisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
copySubtitle: false,
|
||||
copySubtitleMultiple: false,
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
refreshKnownWords: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
debug: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
const calls: string[] = [];
|
||||
let mpvSocketPath = "/tmp/subminer.sock";
|
||||
let texthookerPort = 5174;
|
||||
const osd: string[] = [];
|
||||
|
||||
const deps: CliCommandServiceDeps = {
|
||||
getMpvSocketPath: () => mpvSocketPath,
|
||||
setMpvSocketPath: (socketPath) => {
|
||||
mpvSocketPath = socketPath;
|
||||
calls.push(`setMpvSocketPath:${socketPath}`);
|
||||
},
|
||||
setMpvClientSocketPath: (socketPath) => {
|
||||
calls.push(`setMpvClientSocketPath:${socketPath}`);
|
||||
},
|
||||
hasMpvClient: () => true,
|
||||
connectMpvClient: () => {
|
||||
calls.push("connectMpvClient");
|
||||
},
|
||||
isTexthookerRunning: () => false,
|
||||
setTexthookerPort: (port) => {
|
||||
texthookerPort = port;
|
||||
calls.push(`setTexthookerPort:${port}`);
|
||||
},
|
||||
getTexthookerPort: () => texthookerPort,
|
||||
shouldOpenTexthookerBrowser: () => true,
|
||||
ensureTexthookerRunning: (port) => {
|
||||
calls.push(`ensureTexthookerRunning:${port}`);
|
||||
},
|
||||
openTexthookerInBrowser: (url) => {
|
||||
calls.push(`openTexthookerInBrowser:${url}`);
|
||||
},
|
||||
stopApp: () => {
|
||||
calls.push("stopApp");
|
||||
},
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
initializeOverlayRuntime: () => {
|
||||
calls.push("initializeOverlayRuntime");
|
||||
},
|
||||
toggleVisibleOverlay: () => {
|
||||
calls.push("toggleVisibleOverlay");
|
||||
},
|
||||
toggleInvisibleOverlay: () => {
|
||||
calls.push("toggleInvisibleOverlay");
|
||||
},
|
||||
openYomitanSettingsDelayed: (delayMs) => {
|
||||
calls.push(`openYomitanSettingsDelayed:${delayMs}`);
|
||||
},
|
||||
setVisibleOverlayVisible: (visible) => {
|
||||
calls.push(`setVisibleOverlayVisible:${visible}`);
|
||||
},
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
calls.push(`setInvisibleOverlayVisible:${visible}`);
|
||||
},
|
||||
copyCurrentSubtitle: () => {
|
||||
calls.push("copyCurrentSubtitle");
|
||||
},
|
||||
startPendingMultiCopy: (timeoutMs) => {
|
||||
calls.push(`startPendingMultiCopy:${timeoutMs}`);
|
||||
},
|
||||
mineSentenceCard: async () => {
|
||||
calls.push("mineSentenceCard");
|
||||
},
|
||||
startPendingMineSentenceMultiple: (timeoutMs) => {
|
||||
calls.push(`startPendingMineSentenceMultiple:${timeoutMs}`);
|
||||
},
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push("updateLastCardFromClipboard");
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
calls.push("refreshKnownWords");
|
||||
},
|
||||
cycleSecondarySubMode: () => {
|
||||
calls.push("cycleSecondarySubMode");
|
||||
},
|
||||
triggerFieldGrouping: async () => {
|
||||
calls.push("triggerFieldGrouping");
|
||||
},
|
||||
triggerSubsyncFromConfig: async () => {
|
||||
calls.push("triggerSubsyncFromConfig");
|
||||
},
|
||||
markLastCardAsAudioCard: async () => {
|
||||
calls.push("markLastCardAsAudioCard");
|
||||
},
|
||||
openRuntimeOptionsPalette: () => {
|
||||
calls.push("openRuntimeOptionsPalette");
|
||||
},
|
||||
printHelp: () => {
|
||||
calls.push("printHelp");
|
||||
},
|
||||
hasMainWindow: () => true,
|
||||
getMultiCopyTimeoutMs: () => 2500,
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
log: (message) => {
|
||||
calls.push(`log:${message}`);
|
||||
},
|
||||
warn: (message) => {
|
||||
calls.push(`warn:${message}`);
|
||||
},
|
||||
error: (message) => {
|
||||
calls.push(`error:${message}`);
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return { deps, calls, osd };
|
||||
}
|
||||
|
||||
test("handleCliCommand ignores --start for second-instance without actions", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ start: true });
|
||||
|
||||
handleCliCommand(args, "second-instance", deps);
|
||||
|
||||
assert.ok(calls.includes("log:Ignoring --start because SubMiner is already running."));
|
||||
assert.equal(calls.some((value) => value.includes("connectMpvClient")), false);
|
||||
});
|
||||
|
||||
test("handleCliCommand runs texthooker flow with browser open", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ texthooker: true });
|
||||
|
||||
handleCliCommand(args, "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("ensureTexthookerRunning:5174"));
|
||||
assert.ok(
|
||||
calls.includes("openTexthookerInBrowser:http://127.0.0.1:5174"),
|
||||
);
|
||||
});
|
||||
|
||||
test("handleCliCommand reports async mine errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
mineSentenceCard: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommand(makeArgs({ mineSentence: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(calls.some((value) => value.startsWith("error:mineSentenceCard failed:")));
|
||||
assert.ok(osd.some((value) => value.includes("Mine sentence failed: boom")));
|
||||
});
|
||||
|
||||
test("handleCliCommand applies socket path and connects on start", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommand(
|
||||
makeArgs({ start: true, socketPath: "/tmp/custom.sock" }),
|
||||
"initial",
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.ok(calls.includes("setMpvSocketPath:/tmp/custom.sock"));
|
||||
assert.ok(calls.includes("setMpvClientSocketPath:/tmp/custom.sock"));
|
||||
assert.ok(calls.includes("connectMpvClient"));
|
||||
});
|
||||
|
||||
test("handleCliCommand warns when texthooker port override used while running", () => {
|
||||
const { deps, calls } = createDeps({
|
||||
isTexthookerRunning: () => true,
|
||||
});
|
||||
|
||||
handleCliCommand(
|
||||
makeArgs({ texthookerPort: 9999, texthooker: true }),
|
||||
"initial",
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
calls.includes(
|
||||
"warn:Ignoring --port override because the texthooker server is already running.",
|
||||
),
|
||||
);
|
||||
assert.equal(calls.some((value) => value === "setTexthookerPort:9999"), false);
|
||||
});
|
||||
|
||||
test("handleCliCommand prints help and stops app when no window exists", () => {
|
||||
const { deps, calls } = createDeps({
|
||||
hasMainWindow: () => false,
|
||||
});
|
||||
|
||||
handleCliCommand(makeArgs({ help: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("printHelp"));
|
||||
assert.ok(calls.includes("stopApp"));
|
||||
});
|
||||
|
||||
test("handleCliCommand reports async trigger-subsync errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
triggerSubsyncFromConfig: async () => {
|
||||
throw new Error("subsync boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommand(makeArgs({ triggerSubsync: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
calls.some((value) => value.startsWith("error:triggerSubsyncFromConfig failed:")),
|
||||
);
|
||||
assert.ok(osd.some((value) => value.includes("Subsync failed: subsync boom")));
|
||||
});
|
||||
|
||||
test("handleCliCommand stops app for --stop command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommand(makeArgs({ stop: true }), "initial", deps);
|
||||
assert.ok(calls.includes("log:Stopping SubMiner..."));
|
||||
assert.ok(calls.includes("stopApp"));
|
||||
});
|
||||
|
||||
test("handleCliCommand still runs non-start actions on second-instance", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommand(
|
||||
makeArgs({ start: true, toggleVisibleOverlay: true }),
|
||||
"second-instance",
|
||||
deps,
|
||||
);
|
||||
assert.ok(calls.includes("toggleVisibleOverlay"));
|
||||
assert.equal(calls.some((value) => value === "connectMpvClient"), true);
|
||||
});
|
||||
|
||||
test("handleCliCommand handles visibility and utility command dispatches", () => {
|
||||
const cases: Array<{
|
||||
args: Partial<CliArgs>;
|
||||
expected: string;
|
||||
}> = [
|
||||
{ args: { toggleInvisibleOverlay: true }, expected: "toggleInvisibleOverlay" },
|
||||
{ args: { settings: true }, expected: "openYomitanSettingsDelayed:1000" },
|
||||
{ args: { showVisibleOverlay: true }, expected: "setVisibleOverlayVisible:true" },
|
||||
{ args: { hideVisibleOverlay: true }, expected: "setVisibleOverlayVisible:false" },
|
||||
{ args: { showInvisibleOverlay: true }, expected: "setInvisibleOverlayVisible:true" },
|
||||
{ args: { hideInvisibleOverlay: true }, expected: "setInvisibleOverlayVisible:false" },
|
||||
{ args: { copySubtitle: true }, expected: "copyCurrentSubtitle" },
|
||||
{ args: { copySubtitleMultiple: true }, expected: "startPendingMultiCopy:2500" },
|
||||
{
|
||||
args: { mineSentenceMultiple: true },
|
||||
expected: "startPendingMineSentenceMultiple:2500",
|
||||
},
|
||||
{ args: { toggleSecondarySub: true }, expected: "cycleSecondarySubMode" },
|
||||
{ args: { openRuntimeOptions: true }, expected: "openRuntimeOptionsPalette" },
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommand(makeArgs(entry.args), "initial", deps);
|
||||
assert.ok(
|
||||
calls.includes(entry.expected),
|
||||
`expected call missing for args ${JSON.stringify(entry.args)}: ${entry.expected}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("handleCliCommand runs refresh-known-words command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("refreshKnownWords"));
|
||||
});
|
||||
|
||||
test("handleCliCommand reports async refresh-known-words errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
refreshKnownWords: async () => {
|
||||
throw new Error("refresh boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
calls.some((value) => value.startsWith("error:refreshKnownWords failed:")),
|
||||
);
|
||||
assert.ok(osd.some((value) => value.includes("Refresh known words failed: refresh boom")));
|
||||
});
|
||||
Reference in New Issue
Block a user