import test from 'node:test'; import assert from 'node:assert/strict'; import { ensureLauncherSetupReady, waitForSetupCompletion } from './setup-gate'; import type { SetupState } from '../src/shared/setup-state'; /** An in-progress setup state; tests override only the fields that matter. */ function makeState(overrides: Partial = {}): SetupState { return { version: 4, status: 'in_progress', completedAt: null, completionSource: null, yomitanSetupMode: null, lastSeenYomitanDictionaryCount: 0, pluginInstallStatus: 'unknown', pluginInstallPathSummary: null, windowsMpvShortcutPreferences: { startMenuEnabled: true, desktopEnabled: true }, windowsMpvShortcutLastStatus: 'unknown', bunInstallStatus: 'unknown', launcherInstallStatus: 'unknown', launcherInstallPath: null, ...overrides, }; } function completedState(overrides: Partial = {}): SetupState { return makeState({ status: 'completed', completedAt: '2026-03-07T00:00:00.000Z', completionSource: 'user', yomitanSetupMode: 'internal', lastSeenYomitanDictionaryCount: 1, ...overrides, }); } const cancelledState = () => makeState({ status: 'cancelled' }); /** Deterministic clock: every `now()` advances 100ms and `sleep` returns immediately. */ function fakeClock() { let value = 0; return { sleep: async () => undefined, now: () => (value += 100), timeoutMs: 5_000, pollIntervalMs: 100, }; } /** Reader that serves `states` in order, then repeats the last one; counts its reads. */ function sequenceReader(states: Array) { const reader = { reads: 0, read: (): SetupState | null => states[Math.min(reader.reads++, states.length - 1)] ?? null, }; return reader; } for (const c of [ { name: 'completed', last: completedState(), expected: 'completed' }, { name: 'cancelled', last: cancelledState(), expected: 'cancelled' }, ] as const) { test(`waitForSetupCompletion resolves ${c.name} state after polling through earlier states`, async () => { const reader = sequenceReader([null, makeState(), c.last]); const result = await waitForSetupCompletion({ readSetupState: reader.read, ...fakeClock(), }); assert.equal(result, c.expected); assert.equal(reader.reads, 3); }); } test('ensureLauncherSetupReady launches setup app and resumes only after completion', async () => { const calls: string[] = []; const reader = sequenceReader([ null, makeState(), completedState({ pluginInstallStatus: 'installed', pluginInstallPathSummary: '/tmp/mpv' }), ]); const ready = await ensureLauncherSetupReady({ readSetupState: reader.read, launchSetupApp: () => { calls.push('launch'); }, ...fakeClock(), }); assert.equal(ready, true); assert.deepEqual(calls, ['launch']); }); test('ensureLauncherSetupReady bypasses setup gate when external yomitan is configured', async () => { const calls: string[] = []; const ready = await ensureLauncherSetupReady({ readSetupState: () => null, isExternalYomitanConfigured: () => true, launchSetupApp: () => { calls.push('launch'); }, ...fakeClock(), }); assert.equal(ready, true); assert.deepEqual(calls, []); }); test('ensureLauncherSetupReady waits for finish after legacy mpv plugin removal', async () => { const calls: string[] = []; let legacyPluginInstalled = true; // The completion timestamp moves forward once the setup app has finished. const reader = sequenceReader([ completedState({ yomitanSetupMode: null, lastSeenYomitanDictionaryCount: 0 }), completedState({ yomitanSetupMode: null, lastSeenYomitanDictionaryCount: 0 }), completedState({ completedAt: '2026-05-12T14:40:00.000Z', yomitanSetupMode: null, lastSeenYomitanDictionaryCount: 0, }), ]); const ready = await ensureLauncherSetupReady({ readSetupState: reader.read, hasLegacyMpvPlugin: () => legacyPluginInstalled, launchSetupApp: () => { calls.push('launch'); legacyPluginInstalled = false; }, ...fakeClock(), }); assert.equal(ready, true); assert.deepEqual(calls, ['launch']); assert.equal(reader.reads >= 3, true); }); test('ensureLauncherSetupReady lets users continue without removing a legacy mpv plugin', async () => { const calls: string[] = []; const reader = sequenceReader([ completedState({ lastSeenYomitanDictionaryCount: 2 }), completedState({ lastSeenYomitanDictionaryCount: 2 }), completedState({ completedAt: '2026-05-12T14:30:00.000Z', lastSeenYomitanDictionaryCount: 2 }), ]); const ready = await ensureLauncherSetupReady({ readSetupState: reader.read, hasLegacyMpvPlugin: () => true, launchSetupApp: () => { calls.push('launch'); }, ...fakeClock(), }); assert.equal(ready, true); assert.deepEqual(calls, ['launch']); }); test('ensureLauncherSetupReady fails on timeout/cancelled state', async () => { const result = await ensureLauncherSetupReady({ readSetupState: cancelledState, launchSetupApp: () => undefined, ...fakeClock(), }); assert.equal(result, false); }); test('ensureLauncherSetupReady ignores stale cancelled state after launching setup app', async () => { const reader = sequenceReader([ cancelledState(), cancelledState(), makeState(), completedState({ completionSource: 'legacy_auto_detected', pluginInstallStatus: 'installed', pluginInstallPathSummary: '/tmp/mpv', }), ]); const result = await ensureLauncherSetupReady({ readSetupState: reader.read, launchSetupApp: () => undefined, ...fakeClock(), }); assert.equal(result, true); });