mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-10 04:19:25 -07:00
fix: stabilize Windows launcher and subsync tests
This commit is contained in:
@@ -4,6 +4,7 @@ import { resolveConfigFilePath } from '../src/config/path-resolution.js';
|
|||||||
|
|
||||||
export function resolveMainConfigPath(): string {
|
export function resolveMainConfigPath(): string {
|
||||||
return resolveConfigFilePath({
|
return resolveConfigFilePath({
|
||||||
|
appDataDir: process.env.APPDATA,
|
||||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||||
homeDir: os.homedir(),
|
homeDir: os.homedir(),
|
||||||
existsSync: fs.existsSync,
|
existsSync: fs.existsSync,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { resolveConfigFilePath } from '../../src/config/path-resolution.js';
|
|||||||
|
|
||||||
export function resolveLauncherMainConfigPath(): string {
|
export function resolveLauncherMainConfigPath(): string {
|
||||||
return resolveConfigFilePath({
|
return resolveConfigFilePath({
|
||||||
|
appDataDir: process.env.APPDATA,
|
||||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||||
homeDir: os.homedir(),
|
homeDir: os.homedir(),
|
||||||
existsSync: fs.existsSync,
|
existsSync: fs.existsSync,
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ function makeTestEnv(homeDir: string, xdgConfigHome: string): NodeJS.ProcessEnv
|
|||||||
return {
|
return {
|
||||||
...process.env,
|
...process.env,
|
||||||
HOME: homeDir,
|
HOME: homeDir,
|
||||||
|
USERPROFILE: homeDir,
|
||||||
|
APPDATA: xdgConfigHome,
|
||||||
|
LOCALAPPDATA: path.join(homeDir, 'AppData', 'Local'),
|
||||||
XDG_CONFIG_HOME: xdgConfigHome,
|
XDG_CONFIG_HOME: xdgConfigHome,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,6 +84,7 @@ test('config discovery ignores lowercase subminer candidate', () => {
|
|||||||
const resolved = resolveConfigFilePath({
|
const resolved = resolveConfigFilePath({
|
||||||
xdgConfigHome,
|
xdgConfigHome,
|
||||||
homeDir,
|
homeDir,
|
||||||
|
platform: 'linux',
|
||||||
existsSync: (candidate) => foundPaths.has(path.normalize(candidate)),
|
existsSync: (candidate) => foundPaths.has(path.normalize(candidate)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -528,15 +532,20 @@ test('parseJellyfinPreviewAuthResponse returns null for invalid payloads', () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('deriveJellyfinTokenStorePath resolves alongside config path', () => {
|
test('deriveJellyfinTokenStorePath resolves alongside config path', () => {
|
||||||
const tokenPath = deriveJellyfinTokenStorePath('/home/test/.config/SubMiner/config.jsonc');
|
const configPath = path.join('/home/test', '.config', 'SubMiner', 'config.jsonc');
|
||||||
assert.equal(tokenPath, '/home/test/.config/SubMiner/jellyfin-token-store.json');
|
const tokenPath = deriveJellyfinTokenStorePath(configPath);
|
||||||
|
assert.equal(tokenPath, path.join(path.dirname(configPath), 'jellyfin-token-store.json'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hasStoredJellyfinSession checks token-store existence', () => {
|
test('hasStoredJellyfinSession checks token-store existence', () => {
|
||||||
const exists = (candidate: string): boolean =>
|
const configPath = path.join('/home/test', '.config', 'SubMiner', 'config.jsonc');
|
||||||
candidate === '/home/test/.config/SubMiner/jellyfin-token-store.json';
|
const tokenPath = deriveJellyfinTokenStorePath(configPath);
|
||||||
assert.equal(hasStoredJellyfinSession('/home/test/.config/SubMiner/config.jsonc', exists), true);
|
const exists = (candidate: string): boolean => candidate === tokenPath;
|
||||||
assert.equal(hasStoredJellyfinSession('/home/test/.config/Other/alt.jsonc', exists), false);
|
assert.equal(hasStoredJellyfinSession(configPath, exists), true);
|
||||||
|
assert.equal(
|
||||||
|
hasStoredJellyfinSession(path.join('/home/test', '.config', 'Other', 'alt.jsonc'), exists),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shouldRetryWithStartForNoRunningInstance matches expected app lifecycle error', () => {
|
test('shouldRetryWithStartForNoRunningInstance matches expected app lifecycle error', () => {
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export const state = {
|
|||||||
stopRequested: false,
|
stopRequested: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SpawnTarget = {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
||||||
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
||||||
const OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS = 700;
|
const OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS = 700;
|
||||||
@@ -682,8 +687,56 @@ function buildAppEnv(): NodeJS.ProcessEnv {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeCaptureAppArgs(appArgs: string[]): boolean {
|
||||||
|
const capturePath = process.env.SUBMINER_TEST_CAPTURE?.trim();
|
||||||
|
if (!capturePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(capturePath, `${appArgs.join('\n')}${appArgs.length > 0 ? '\n' : ''}`, 'utf8');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAppSpawnTarget(appPath: string, appArgs: string[]): SpawnTarget {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return { command: appPath, args: appArgs };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBashArg = (value: string): string => {
|
||||||
|
const normalized = value.replace(/\\/g, '/');
|
||||||
|
const driveMatch = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
||||||
|
if (!driveMatch) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, driveLetter, remainder] = driveMatch;
|
||||||
|
return `/mnt/${driveLetter!.toLowerCase()}/${remainder}`;
|
||||||
|
};
|
||||||
|
const extension = path.extname(appPath).toLowerCase();
|
||||||
|
if (extension === '.ps1') {
|
||||||
|
return {
|
||||||
|
command: 'powershell.exe',
|
||||||
|
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', appPath, ...appArgs],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === '.sh') {
|
||||||
|
return {
|
||||||
|
command: 'bash',
|
||||||
|
args: [normalizeBashArg(appPath), ...appArgs.map(normalizeBashArg)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command: appPath, args: appArgs };
|
||||||
|
}
|
||||||
|
|
||||||
export function runAppCommandWithInherit(appPath: string, appArgs: string[]): never {
|
export function runAppCommandWithInherit(appPath: string, appArgs: string[]): never {
|
||||||
const result = spawnSync(appPath, appArgs, {
|
if (maybeCaptureAppArgs(appArgs)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolveAppSpawnTarget(appPath, appArgs);
|
||||||
|
const result = spawnSync(target.command, target.args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: buildAppEnv(),
|
env: buildAppEnv(),
|
||||||
});
|
});
|
||||||
@@ -702,7 +755,16 @@ export function runAppCommandCaptureOutput(
|
|||||||
stderr: string;
|
stderr: string;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
} {
|
} {
|
||||||
const result = spawnSync(appPath, appArgs, {
|
if (maybeCaptureAppArgs(appArgs)) {
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
stdout: '',
|
||||||
|
stderr: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolveAppSpawnTarget(appPath, appArgs);
|
||||||
|
const result = spawnSync(target.command, target.args, {
|
||||||
env: buildAppEnv(),
|
env: buildAppEnv(),
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
@@ -721,8 +783,17 @@ export function runAppCommandWithInheritLogged(
|
|||||||
logLevel: LogLevel,
|
logLevel: LogLevel,
|
||||||
label: string,
|
label: string,
|
||||||
): never {
|
): never {
|
||||||
log('debug', logLevel, `${label}: launching app with args: ${appArgs.join(' ')}`);
|
if (maybeCaptureAppArgs(appArgs)) {
|
||||||
const result = spawnSync(appPath, appArgs, {
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolveAppSpawnTarget(appPath, appArgs);
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
logLevel,
|
||||||
|
`${label}: launching app with args: ${[target.command, ...target.args].join(' ')}`,
|
||||||
|
);
|
||||||
|
const result = spawnSync(target.command, target.args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: buildAppEnv(),
|
env: buildAppEnv(),
|
||||||
});
|
});
|
||||||
@@ -736,7 +807,11 @@ export function runAppCommandWithInheritLogged(
|
|||||||
export function launchAppStartDetached(appPath: string, logLevel: LogLevel): void {
|
export function launchAppStartDetached(appPath: string, logLevel: LogLevel): void {
|
||||||
const startArgs = ['--start'];
|
const startArgs = ['--start'];
|
||||||
if (logLevel !== 'info') startArgs.push('--log-level', logLevel);
|
if (logLevel !== 'info') startArgs.push('--log-level', logLevel);
|
||||||
const proc = spawn(appPath, startArgs, {
|
if (maybeCaptureAppArgs(startArgs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = resolveAppSpawnTarget(appPath, startArgs);
|
||||||
|
const proc = spawn(target.command, target.args, {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
detached: true,
|
detached: true,
|
||||||
env: buildAppEnv(),
|
env: buildAppEnv(),
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ paths=(
|
|||||||
"src"
|
"src"
|
||||||
)
|
)
|
||||||
|
|
||||||
exec bunx prettier "$@" "${paths[@]}"
|
BUN_BIN="$(command -v bun.exe || command -v bun)"
|
||||||
|
exec "$BUN_BIN" x prettier "$@" "${paths[@]}"
|
||||||
|
|||||||
@@ -147,6 +147,28 @@ function writeExecutableScript(filePath: string, content: string): void {
|
|||||||
fs.chmodSync(filePath, 0o755);
|
fs.chmodSync(filePath, 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toShellPath(filePath: string): string {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath.replace(/\\/g, '/').replace(/^([A-Za-z]):\//, (_, driveLetter: string) => {
|
||||||
|
return `/mnt/${driveLetter.toLowerCase()}/`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromShellPath(filePath: string): string {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath
|
||||||
|
.replace(/^\/mnt\/([a-z])\//, (_, driveLetter: string) => {
|
||||||
|
return `${driveLetter.toUpperCase()}:/`;
|
||||||
|
})
|
||||||
|
.replace(/\//g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
test('runSubsyncManual constructs ffsubsync command and returns success', async () => {
|
test('runSubsyncManual constructs ffsubsync command and returns success', async () => {
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subsync-ffsubsync-'));
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subsync-ffsubsync-'));
|
||||||
const ffsubsyncLogPath = path.join(tmpDir, 'ffsubsync-args.log');
|
const ffsubsyncLogPath = path.join(tmpDir, 'ffsubsync-args.log');
|
||||||
@@ -162,7 +184,7 @@ test('runSubsyncManual constructs ffsubsync command and returns success', async
|
|||||||
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
||||||
writeExecutableScript(
|
writeExecutableScript(
|
||||||
ffsubsyncPath,
|
ffsubsyncPath,
|
||||||
`#!/bin/sh\n: > "${ffsubsyncLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${ffsubsyncLogPath}"; done\nout=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$out\" ]; then : > \"$out\"; fi\nexit 0\n`,
|
`#!/bin/sh\n: > "${toShellPath(ffsubsyncLogPath)}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${toShellPath(ffsubsyncLogPath)}"; done\nout=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$out\" ]; then : > \"$out\"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sentCommands: Array<Array<string | number>> = [];
|
const sentCommands: Array<Array<string | number>> = [];
|
||||||
@@ -204,14 +226,14 @@ test('runSubsyncManual constructs ffsubsync command and returns success', async
|
|||||||
assert.equal(result.ok, true);
|
assert.equal(result.ok, true);
|
||||||
assert.equal(result.message, 'Subtitle synchronized with ffsubsync');
|
assert.equal(result.message, 'Subtitle synchronized with ffsubsync');
|
||||||
const ffArgs = fs.readFileSync(ffsubsyncLogPath, 'utf8').trim().split('\n');
|
const ffArgs = fs.readFileSync(ffsubsyncLogPath, 'utf8').trim().split('\n');
|
||||||
assert.equal(ffArgs[0], videoPath);
|
assert.equal(ffArgs[0], toShellPath(videoPath));
|
||||||
assert.ok(ffArgs.includes('-i'));
|
assert.ok(ffArgs.includes('-i'));
|
||||||
assert.ok(ffArgs.includes(primaryPath));
|
assert.ok(ffArgs.includes(toShellPath(primaryPath)));
|
||||||
assert.ok(ffArgs.includes('--reference-stream'));
|
assert.ok(ffArgs.includes('--reference-stream'));
|
||||||
assert.ok(ffArgs.includes('0:2'));
|
assert.ok(ffArgs.includes('0:2'));
|
||||||
const ffOutputFlagIndex = ffArgs.indexOf('-o');
|
const ffOutputFlagIndex = ffArgs.indexOf('-o');
|
||||||
assert.equal(ffOutputFlagIndex >= 0, true);
|
assert.equal(ffOutputFlagIndex >= 0, true);
|
||||||
assert.equal(ffArgs[ffOutputFlagIndex + 1], primaryPath);
|
assert.equal(ffArgs[ffOutputFlagIndex + 1], toShellPath(primaryPath));
|
||||||
assert.equal(sentCommands[0]?.[0], 'sub_add');
|
assert.equal(sentCommands[0]?.[0], 'sub_add');
|
||||||
assert.deepEqual(sentCommands[1], ['set_property', 'sub-delay', 0]);
|
assert.deepEqual(sentCommands[1], ['set_property', 'sub-delay', 0]);
|
||||||
});
|
});
|
||||||
@@ -231,7 +253,7 @@ test('runSubsyncManual writes deterministic _retimed filename when replace is fa
|
|||||||
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
||||||
writeExecutableScript(
|
writeExecutableScript(
|
||||||
ffsubsyncPath,
|
ffsubsyncPath,
|
||||||
`#!/bin/sh\n: > "${ffsubsyncLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${ffsubsyncLogPath}"; done\nout=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$out\" ]; then : > \"$out\"; fi\nexit 0\n`,
|
`#!/bin/sh\n: > "${toShellPath(ffsubsyncLogPath)}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${toShellPath(ffsubsyncLogPath)}"; done\nout=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$out\" ]; then : > \"$out\"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deps = makeDeps({
|
const deps = makeDeps({
|
||||||
@@ -273,7 +295,7 @@ test('runSubsyncManual writes deterministic _retimed filename when replace is fa
|
|||||||
const ffOutputFlagIndex = ffArgs.indexOf('-o');
|
const ffOutputFlagIndex = ffArgs.indexOf('-o');
|
||||||
assert.equal(ffOutputFlagIndex >= 0, true);
|
assert.equal(ffOutputFlagIndex >= 0, true);
|
||||||
const outputPath = ffArgs[ffOutputFlagIndex + 1];
|
const outputPath = ffArgs[ffOutputFlagIndex + 1];
|
||||||
assert.equal(outputPath, path.join(tmpDir, 'episode.ja_retimed.srt'));
|
assert.equal(outputPath, toShellPath(path.join(tmpDir, 'episode.ja_retimed.srt')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runSubsyncManual reports ffsubsync command failures with details', async () => {
|
test('runSubsyncManual reports ffsubsync command failures with details', async () => {
|
||||||
@@ -346,7 +368,7 @@ test('runSubsyncManual constructs alass command and returns failure on non-zero
|
|||||||
writeExecutableScript(ffsubsyncPath, '#!/bin/sh\nexit 0\n');
|
writeExecutableScript(ffsubsyncPath, '#!/bin/sh\nexit 0\n');
|
||||||
writeExecutableScript(
|
writeExecutableScript(
|
||||||
alassPath,
|
alassPath,
|
||||||
`#!/bin/sh\n: > "${alassLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${alassLogPath}"; done\nexit 1\n`,
|
`#!/bin/sh\n: > "${toShellPath(alassLogPath)}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${toShellPath(alassLogPath)}"; done\nexit 1\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deps = makeDeps({
|
const deps = makeDeps({
|
||||||
@@ -393,8 +415,8 @@ test('runSubsyncManual constructs alass command and returns failure on non-zero
|
|||||||
assert.equal(typeof result.message, 'string');
|
assert.equal(typeof result.message, 'string');
|
||||||
assert.equal(result.message.startsWith('alass synchronization failed'), true);
|
assert.equal(result.message.startsWith('alass synchronization failed'), true);
|
||||||
const alassArgs = fs.readFileSync(alassLogPath, 'utf8').trim().split('\n');
|
const alassArgs = fs.readFileSync(alassLogPath, 'utf8').trim().split('\n');
|
||||||
assert.equal(alassArgs[0], sourcePath);
|
assert.equal(alassArgs[0], toShellPath(sourcePath));
|
||||||
assert.equal(alassArgs[1], primaryPath);
|
assert.equal(alassArgs[1], toShellPath(primaryPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runSubsyncManual keeps internal alass source file alive until sync finishes', async () => {
|
test('runSubsyncManual keeps internal alass source file alive until sync finishes', async () => {
|
||||||
@@ -482,7 +504,7 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a
|
|||||||
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
writeExecutableScript(alassPath, '#!/bin/sh\nexit 0\n');
|
||||||
writeExecutableScript(
|
writeExecutableScript(
|
||||||
ffsubsyncPath,
|
ffsubsyncPath,
|
||||||
`#!/bin/sh\nmkdir -p "${tmpDir}"\n: > "${ffsubsyncLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${ffsubsyncLogPath}"; done\nprev=""\nout=""\nfor arg in "$@"; do\n if [ "$prev" = "--reference-stream" ]; then :; fi\n if [ "$prev" = "-o" ]; then out="$arg"; fi\n prev="$arg"\ndone\nif [ -n "$out" ]; then : > "$out"; fi`,
|
`#!/bin/sh\nmkdir -p "${toShellPath(tmpDir)}"\n: > "${toShellPath(ffsubsyncLogPath)}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${toShellPath(ffsubsyncLogPath)}"; done\nprev=""\nout=""\nfor arg in "$@"; do\n if [ "$prev" = "--reference-stream" ]; then :; fi\n if [ "$prev" = "-o" ]; then out="$arg"; fi\n prev="$arg"\ndone\nif [ -n "$out" ]; then : > "$out"; fi`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deps = makeDeps({
|
const deps = makeDeps({
|
||||||
@@ -526,5 +548,5 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a
|
|||||||
const outputPath = ffArgs[syncOutputIndex + 1];
|
const outputPath = ffArgs[syncOutputIndex + 1];
|
||||||
assert.equal(typeof outputPath, 'string');
|
assert.equal(typeof outputPath, 'string');
|
||||||
assert.ok(outputPath!.length > 0);
|
assert.ok(outputPath!.length > 0);
|
||||||
assert.equal(fs.readFileSync(outputPath!, 'utf8'), '');
|
assert.equal(fs.readFileSync(fromShellPath(outputPath!), 'utf8'), '');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,18 +8,21 @@ import {
|
|||||||
} from './yomitan-extension-paths';
|
} from './yomitan-extension-paths';
|
||||||
|
|
||||||
test('getYomitanExtensionSearchPaths prioritizes generated build output before packaged fallbacks', () => {
|
test('getYomitanExtensionSearchPaths prioritizes generated build output before packaged fallbacks', () => {
|
||||||
|
const repoRoot = path.resolve('repo');
|
||||||
|
const resourcesPath = path.join(path.sep, 'opt', 'SubMiner', 'resources');
|
||||||
|
const userDataPath = path.join(path.sep, 'Users', 'kyle', '.config', 'SubMiner');
|
||||||
const searchPaths = getYomitanExtensionSearchPaths({
|
const searchPaths = getYomitanExtensionSearchPaths({
|
||||||
cwd: '/repo',
|
cwd: repoRoot,
|
||||||
moduleDir: '/repo/dist/core/services',
|
moduleDir: path.join(repoRoot, 'dist', 'core', 'services'),
|
||||||
resourcesPath: '/opt/SubMiner/resources',
|
resourcesPath,
|
||||||
userDataPath: '/Users/kyle/.config/SubMiner',
|
userDataPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(searchPaths, [
|
assert.deepEqual(searchPaths, [
|
||||||
path.join('/repo', 'build', 'yomitan'),
|
path.join(repoRoot, 'build', 'yomitan'),
|
||||||
path.join('/opt/SubMiner/resources', 'yomitan'),
|
path.join(resourcesPath, 'yomitan'),
|
||||||
'/usr/share/SubMiner/yomitan',
|
'/usr/share/SubMiner/yomitan',
|
||||||
path.join('/Users/kyle/.config/SubMiner', 'yomitan'),
|
path.join(userDataPath, 'yomitan'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
import { DEFAULT_CONFIG } from '../config';
|
import { DEFAULT_CONFIG } from '../config';
|
||||||
import { SubsyncConfig, SubsyncMode } from '../types';
|
import { SubsyncConfig, SubsyncMode } from '../types';
|
||||||
|
|
||||||
@@ -45,6 +46,42 @@ export interface CommandResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCommandInvocation(
|
||||||
|
executable: string,
|
||||||
|
args: string[],
|
||||||
|
): { command: string; args: string[] } {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return { command: executable, args };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBashArg = (value: string): string => {
|
||||||
|
const normalized = value.replace(/\\/g, '/');
|
||||||
|
const driveMatch = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
||||||
|
if (!driveMatch) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, driveLetter, remainder] = driveMatch;
|
||||||
|
return `/mnt/${driveLetter!.toLowerCase()}/${remainder}`;
|
||||||
|
};
|
||||||
|
const extension = path.extname(executable).toLowerCase();
|
||||||
|
if (extension === '.ps1') {
|
||||||
|
return {
|
||||||
|
command: 'powershell.exe',
|
||||||
|
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', executable, ...args],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === '.sh') {
|
||||||
|
return {
|
||||||
|
command: 'bash',
|
||||||
|
args: [normalizeBashArg(executable), ...args.map(normalizeBashArg)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command: executable, args };
|
||||||
|
}
|
||||||
|
|
||||||
export function getSubsyncConfig(config: SubsyncConfig | undefined): SubsyncResolvedConfig {
|
export function getSubsyncConfig(config: SubsyncConfig | undefined): SubsyncResolvedConfig {
|
||||||
const resolvePath = (value: string | undefined, fallback: string): string => {
|
const resolvePath = (value: string | undefined, fallback: string): string => {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
@@ -108,7 +145,8 @@ export function runCommand(
|
|||||||
timeoutMs = 120000,
|
timeoutMs = 120000,
|
||||||
): Promise<CommandResult> {
|
): Promise<CommandResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = childProcess.spawn(executable, args, {
|
const invocation = resolveCommandInvocation(executable, args);
|
||||||
|
const child = childProcess.spawn(invocation.command, invocation.args, {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user