Files
SubMiner/src/main/runtime/session-bindings-runtime.test.ts
T

139 lines
5.3 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import type { CompiledSessionBinding, ResolvedConfig } from '../../types';
import { createSessionBindingsRuntime } from './session-bindings-runtime';
test('persistSessionBindings logs and does not publish bindings when artifact write fails', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-bindings-runtime-'));
const configDir = path.join(root, 'config-file');
fs.writeFileSync(configDir, 'not a directory');
const calls: string[] = [];
const runtime = createSessionBindingsRuntime({
configDir,
getKeybindings: () => [],
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
getResolvedConfig: () =>
({
stats: { toggleKey: 's', markWatchedKey: 'w' },
}) as ResolvedConfig,
getMpvClient: () => null,
setSessionBindings: () => calls.push('setSessionBindings'),
setSessionBindingsInitialized: () => calls.push('setSessionBindingsInitialized'),
logWarn: (message) => calls.push(`warn:${message}`),
});
try {
assert.throws(
() => runtime.persistSessionBindings([] as CompiledSessionBinding[]),
/ENOTDIR|EEXIST/,
);
assert.deepEqual(calls, ['warn:[session-bindings] Failed to write session bindings artifact']);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('persistSessionBindings keeps saved bindings when mpv reload notification fails', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-bindings-runtime-'));
const calls: string[] = [];
const runtime = createSessionBindingsRuntime({
configDir: root,
getKeybindings: () => [],
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
getResolvedConfig: () =>
({
stats: { toggleKey: 's', markWatchedKey: 'w' },
}) as ResolvedConfig,
getMpvClient: () =>
({
connected: true,
send: () => {
throw new Error('mpv unavailable');
},
}) as never,
setSessionBindings: () => calls.push('setSessionBindings'),
setSessionBindingsInitialized: () => calls.push('setSessionBindingsInitialized'),
logWarn: (message) => calls.push(`warn:${message}`),
});
try {
assert.doesNotThrow(() => runtime.persistSessionBindings([] as CompiledSessionBinding[]));
assert.deepEqual(calls, [
'setSessionBindings',
'setSessionBindingsInitialized',
'warn:[session-bindings] Failed to notify mpv to reload session bindings',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('native prefix conflicts publish the same effective bindings to the overlay and plugin and recover', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-conflict-'));
const sequence: CompiledSessionBinding = {
sourcePath: 'shortcuts.openSubtitleSelection',
originalKey: 'g-s',
key: { code: 'KeyG-KeyS', modifiers: [] },
actionType: 'session-action',
actionId: 'openSubtitleSelection',
};
let nativeKeys: unknown = [];
let failDiscovery = false;
let published: CompiledSessionBinding[] = [];
const events: CompiledSessionBinding[][] = [];
const warnings: string[] = [];
const client = {
connected: true,
send: () => {},
requestProperty: async () => {
if (failDiscovery) throw new Error('temporarily unavailable');
return nativeKeys;
},
};
const runtime = createSessionBindingsRuntime({
configDir: root,
getKeybindings: () => [],
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
getResolvedConfig: () => ({ stats: { toggleKey: 's', markWatchedKey: 'w' } }) as ResolvedConfig,
getMpvClient: () => client,
setSessionBindings: (bindings) => {
published = bindings;
},
setSessionBindingsInitialized: () => {},
logWarn: () => {},
onBindingsChanged: (bindings) => events.push(bindings),
onWarning: (warning) => warnings.push(warning.message),
});
const readArtifact = () =>
JSON.parse(fs.readFileSync(path.join(root, 'session-bindings.json'), 'utf8'));
try {
runtime.persistSessionBindings([sequence]);
nativeKeys = [{ key: 'g', cmd: 'show-text single', priority: 1 }];
await runtime.refreshMpvSessionBindings();
assert.deepEqual(published, []);
assert.deepEqual(events.at(-1), readArtifact().bindings);
assert.equal(warnings.length, 1);
assert.match(warnings[0]!, /mpv input binding "g"/);
await runtime.refreshMpvSessionBindings();
assert.equal(events.length, 2, 'unchanged discovery must not create a reload loop');
assert.equal(warnings.length, 1);
failDiscovery = true;
await runtime.refreshMpvSessionBindings();
assert.deepEqual(published, [], 'failed discovery retains the known conflict');
failDiscovery = false;
nativeKeys = [{ key: 'Shift+g', cmd: 'show-text shifted', priority: 1 }];
await runtime.refreshMpvSessionBindings();
assert.deepEqual(published, [sequence]);
assert.equal(readArtifact().bindings[0].key.code, 'KeyG-KeyS');
assert.deepEqual(readArtifact().warnings, []);
client.connected = false;
await runtime.refreshMpvSessionBindings();
assert.deepEqual(published, [sequence]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});