From 51aac9b7a1ca84a1b84497414050d188aa80b4af Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 14 Apr 2026 21:44:03 +0300 Subject: [PATCH] fix(tmux): strengthen windows restart detection --- .../runtime/TmuxInstallerRunnerAdapter.ts | 27 ++++- .../TmuxInstallerRunnerAdapter.test.ts | 96 +++++++++++++++++ .../main/infrastructure/wsl/TmuxWslService.ts | 102 +++++++++++++++++- .../wsl/WindowsElevatedStepRunner.ts | 24 ++++- .../wsl/__tests__/TmuxWslService.test.ts | 77 +++++++++++++ .../WindowsElevatedStepRunner.test.ts | 33 +++++- .../adapters/TmuxInstallerBannerAdapter.ts | 24 ++--- .../TmuxInstallerBannerAdapter.test.ts | 36 ++++++- 8 files changed, 392 insertions(+), 27 deletions(-) diff --git a/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts b/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts index 8697e8db..f484ba55 100644 --- a/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts +++ b/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts @@ -17,6 +17,7 @@ const RETRY_WITH_UPDATE_PATTERNS = ['unable to locate package', 'failed to fetch const RECOMMENDED_WSL_DISTRO_NAME = 'Ubuntu'; const WINDOWS_DISTRO_APPEAR_RETRY_DELAY_MS = 2_000; const WINDOWS_DISTRO_APPEAR_RETRY_ATTEMPTS = 6; +const RESTART_REQUIRED_PATTERNS = ['restart', 'reboot', 'перезагруз', 'требуется перезагрузка']; class TmuxInstallCancelledError extends Error { constructor() { @@ -325,13 +326,28 @@ export class TmuxInstallerRunnerAdapter return status; } - if (status.wsl?.rebootRequired) { + const rebootRequired = + elevationResult.restartRequired || + status.wsl?.rebootRequired || + this.#looksLikeRestartRequired(elevationResult.detail); + if (rebootRequired) { + const rebootStatus = status.wsl + ? { + ...status, + wsl: { + ...status.wsl, + rebootRequired: true, + statusDetail: elevationResult.detail ?? status.wsl.statusDetail, + }, + } + : status; this.#setSnapshot({ phase: 'needs_restart', strategy: 'wsl', message: 'Restart Windows before continuing with tmux setup', detail: - status.wsl.statusDetail ?? + elevationResult.detail ?? + status.wsl?.statusDetail ?? 'WSL was installed, but Windows still needs a restart before tmux setup can continue.', error: null, canCancel: false, @@ -339,7 +355,7 @@ export class TmuxInstallerRunnerAdapter inputPrompt: null, inputSecret: false, }); - return status; + return rebootStatus; } if (!status.wsl?.wslInstalled) { @@ -464,6 +480,11 @@ export class TmuxInstallerRunnerAdapter return status; } + #looksLikeRestartRequired(value: string | null | undefined): boolean { + const lowered = value?.toLowerCase() ?? ''; + return RESTART_REQUIRED_PATTERNS.some((pattern) => lowered.includes(pattern)); + } + async #runResolvedPlan(plan: TmuxInstallPlan, resetLogs = true): Promise { this.#setSnapshot({ phase: 'preparing', diff --git a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts index b71579db..1a976f1a 100644 --- a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts +++ b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts @@ -467,4 +467,100 @@ describe('TmuxInstallerRunnerAdapter', () => { expect(runner.getSnapshot().message).toContain('Finish Ubuntu setup'); expect(runner.getSnapshot().detail).toContain('Wait a moment, then click Re-check'); }); + + it('switches Windows WSL setup into needs_restart when the elevated step reports a localized reboot message', async () => { + const presenter = createPresenter(); + const initialStatus = createBaseStatus({ + platform: 'win32', + autoInstall: { + supported: true, + strategy: 'wsl', + packageManagerLabel: 'WSL', + requiresTerminalInput: false, + requiresAdmin: true, + requiresRestart: true, + mayOpenExternalWindow: true, + reasonIfUnsupported: null, + manualHints: [], + }, + wsl: { + wslInstalled: false, + rebootRequired: false, + distroName: null, + distroVersion: null, + distroBootstrapped: false, + innerPackageManager: null, + tmuxAvailableInsideWsl: false, + tmuxVersion: null, + tmuxBinaryPath: null, + statusDetail: 'WSL is not installed yet.', + }, + }); + const refreshedStatus = createBaseStatus({ + platform: 'win32', + autoInstall: initialStatus.autoInstall, + wsl: { + wslInstalled: true, + rebootRequired: false, + distroName: null, + distroVersion: null, + distroBootstrapped: false, + innerPackageManager: null, + tmuxAvailableInsideWsl: false, + tmuxVersion: null, + tmuxBinaryPath: null, + statusDetail: 'WSL is available, but no Linux distribution is installed yet.', + }, + }); + let statusCallCount = 0; + const statusSource = { + getStatus: vi.fn(async () => { + statusCallCount += 1; + return statusCallCount === 1 ? initialStatus : refreshedStatus; + }), + invalidateStatus: vi.fn(), + }; + const runner = new TmuxInstallerRunnerAdapter( + statusSource as never, + presenter as never, + { + resolve: vi.fn(async () => { + throw new Error('resolve() should not run before restart'); + }), + } as never, + { + run: vi.fn(), + cancel: vi.fn(), + } as never, + { + run: vi.fn(), + writeLine: vi.fn(), + cancel: vi.fn(), + } as never, + { + persistPreferredDistro: vi.fn(async () => undefined), + } as never, + { + runWslCoreInstall: vi.fn(async () => ({ + outcome: 'elevated_succeeded', + detail: 'WSL core installation command completed.', + restartRequired: true, + featureStates: [ + { + featureName: 'VirtualMachinePlatform', + state: 'EnablePending', + restartRequired: 'Possible', + }, + ], + resultFilePath: 'C:\\temp\\result.json', + })), + } as never + ); + + await expect(runner.install()).resolves.toBeUndefined(); + + expect(runner.getSnapshot().phase).toBe('needs_restart'); + expect(runner.getSnapshot().message).toContain('Restart Windows'); + expect(runner.getSnapshot().detail).toContain('WSL core installation command completed'); + }); }); diff --git a/src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts b/src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts index a5dc3740..edbd8a4d 100644 --- a/src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts +++ b/src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts @@ -23,6 +23,17 @@ interface WslVerboseDistroEntry { version: 1 | 2 | null; } +interface WindowsOptionalFeatureState { + FeatureName?: string | null; + State?: string | null; + RestartRequired?: string | boolean | null; +} + +interface WindowsOptionalFeatureProbe { + restartPending: boolean; + hasConfiguredWslFeature: boolean; +} + type ExecFileCallback = ( error: Error | null, stdout: string | Buffer, @@ -48,6 +59,11 @@ export interface TmuxWslProbeResult { const MAX_BUFFER_BYTES = 1024 * 1024; const WSL_NOT_AVAILABLE_DETAIL = 'WSL is not available on this Windows machine yet.'; +const WINDOWS_WSL_FEATURE_NAMES = ['Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform']; +const POWERSHELL_FEATURE_QUERY = [ + '$features = Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Windows-Subsystem-Linux","VirtualMachinePlatform"', + '$features | Select-Object FeatureName, State, RestartRequired | ConvertTo-Json -Compress', +].join('; '); export class TmuxWslService { readonly #execFile: ExecFileLike; @@ -64,11 +80,15 @@ export class TmuxWslService { async probe(): Promise { const statusProbe = await this.#run(['--status'], 4_000); const distroListProbe = await this.#run(['--list', '--quiet'], 4_000); + const featureProbe = await this.#queryWindowsOptionalFeatures(); const persistedPreferredDistro = await this.#preferenceStore.getPreferredDistro(); - const wslInstalled = statusProbe.exitCode === 0 || distroListProbe.exitCode === 0; - const rebootRequired = this.#looksLikeRestartRequired( - `${statusProbe.stdout}\n${statusProbe.stderr}` - ); + const wslInstalled = + statusProbe.exitCode === 0 || + distroListProbe.exitCode === 0 || + featureProbe?.hasConfiguredWslFeature === true; + const rebootRequired = + featureProbe?.restartPending === true || + this.#looksLikeRestartRequired(`${statusProbe.stdout}\n${statusProbe.stderr}`); if (!wslInstalled) { if (persistedPreferredDistro) { @@ -438,7 +458,79 @@ export class TmuxWslService { #looksLikeRestartRequired(output: string): boolean { const lowered = output.toLowerCase(); - return lowered.includes('restart') || lowered.includes('reboot'); + return ( + lowered.includes('restart') || + lowered.includes('reboot') || + lowered.includes('перезагруз') || + lowered.includes('требуется перезагрузка') + ); + } + + async #queryWindowsOptionalFeatures(): Promise { + const result = await this.#execPowerShell( + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', POWERSHELL_FEATURE_QUERY], + 6_000 + ); + if (!result || result.exitCode !== 0 || !result.stdout.trim()) { + return null; + } + + try { + const parsed = JSON.parse(result.stdout) as + | WindowsOptionalFeatureState + | WindowsOptionalFeatureState[]; + const features = Array.isArray(parsed) ? parsed : [parsed]; + const relevantFeatures = features.filter((feature) => + WINDOWS_WSL_FEATURE_NAMES.includes(feature.FeatureName ?? '') + ); + if (relevantFeatures.length === 0) { + return null; + } + + return { + restartPending: relevantFeatures.some((feature) => + String(feature.State ?? '') + .toLowerCase() + .includes('pending') + ), + hasConfiguredWslFeature: relevantFeatures.some((feature) => { + const state = String(feature.State ?? '').toLowerCase(); + return state.length > 0 && state !== 'disabled' && state !== 'disabledwithpayloadremoved'; + }), + }; + } catch { + return null; + } + } + + async #execPowerShell(args: string[], timeout: number): Promise { + return new Promise((resolve) => { + this.#execFile( + 'powershell.exe', + args, + { + timeout, + windowsHide: true, + maxBuffer: MAX_BUFFER_BYTES, + encoding: 'buffer', + }, + (error, stdout, stderr) => { + const errorCode = + typeof error === 'object' && error !== null && 'code' in error + ? (error as NodeJS.ErrnoException).code + : undefined; + if (errorCode === 'ENOENT') { + resolve(null); + return; + } + resolve({ + exitCode: typeof errorCode === 'number' ? errorCode : error ? 1 : 0, + stdout: this.#decodeOutput(stdout), + stderr: this.#decodeOutput(stderr) || (error instanceof Error ? error.message : ''), + }); + } + ); + }); } #firstNonEmpty(...values: (string | null | undefined)[]): string { diff --git a/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts b/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts index 46bc8f8f..0d90b6da 100644 --- a/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts +++ b/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts @@ -15,9 +15,18 @@ interface ExecResult { stderr: string; } +interface PersistedFeatureState { + featureName?: string | null; + state?: string | null; + restartRequired?: string | boolean | null; +} + interface PersistedElevationResult { ok?: boolean; detail?: string | null; + restartRequired?: boolean | null; + featureStates?: PersistedFeatureState[] | null; + commandExitCode?: number | null; } type ExecFileCallback = ( @@ -47,6 +56,8 @@ export interface WindowsElevatedStepResult { | 'elevated_failed' | 'elevated_unknown_outcome'; detail: string | null; + restartRequired: boolean; + featureStates: PersistedFeatureState[]; resultFilePath: string | null; } @@ -84,6 +95,8 @@ export class WindowsElevatedStepRunner { return { outcome: persistedResult.ok ? 'elevated_succeeded' : 'elevated_failed', detail: persistedResult.detail ?? null, + restartRequired: persistedResult.restartRequired === true, + featureStates: persistedResult.featureStates ?? [], resultFilePath, }; } @@ -92,6 +105,8 @@ export class WindowsElevatedStepRunner { return { outcome: 'elevated_cancelled', detail: 'Administrator permission request was cancelled.', + restartRequired: false, + featureStates: [], resultFilePath: null, }; } @@ -103,6 +118,8 @@ export class WindowsElevatedStepRunner { return { outcome: 'elevated_unknown_outcome', detail: this.#firstNonEmpty(result.stderr, result.stdout), + restartRequired: false, + featureStates: [], resultFilePath: null, }; } @@ -170,9 +187,14 @@ export class WindowsElevatedStepRunner { '$ErrorActionPreference = "Stop"', `$resultFile = '${escapedResultFilePath}'`, `$wslArgs = @(${quotedArgs})`, - '$result = @{ ok = $false; detail = $null }', + '$featureNames = @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform")', + '$result = @{ ok = $false; detail = $null; restartRequired = $false; featureStates = @(); commandExitCode = $null }', 'try {', ' & wsl.exe @wslArgs', + ' $result.commandExitCode = $LASTEXITCODE', + ' $features = @(Get-WindowsOptionalFeature -Online -FeatureName $featureNames | Select-Object FeatureName, State, RestartRequired)', + ' $result.featureStates = @($features | ForEach-Object { @{ featureName = $_.FeatureName; state = [string]$_.State; restartRequired = $_.RestartRequired } })', + ' $result.restartRequired = @($features | Where-Object { "$($_.State)" -like "*Pending*" }).Count -gt 0', ' if ($LASTEXITCODE -eq 0) {', ' $result.ok = $true', ' $result.detail = "WSL core installation command completed."', diff --git a/src/features/tmux-installer/main/infrastructure/wsl/__tests__/TmuxWslService.test.ts b/src/features/tmux-installer/main/infrastructure/wsl/__tests__/TmuxWslService.test.ts index f3daee50..e1ed78f5 100644 --- a/src/features/tmux-installer/main/infrastructure/wsl/__tests__/TmuxWslService.test.ts +++ b/src/features/tmux-installer/main/infrastructure/wsl/__tests__/TmuxWslService.test.ts @@ -174,4 +174,81 @@ describe('TmuxWslService', () => { expect(result.preference?.source).toBe('default'); expect(preferenceStore.getPreferredDistroSync()).toBeNull(); }); + + it('detects a reboot requirement from localized Windows output', async () => { + const service = new TmuxWslService( + createExecFileMock({ + '--status': { + stdout: + 'Требуемая операция выполнена успешно. Чтобы сделанные изменения вступили в силу, следует перезагрузить систему.', + }, + '--list --quiet': { stdout: '' }, + }), + createPreferenceStore() as never + ); + + const result = await service.probe(); + + expect(result.status.wslInstalled).toBe(true); + expect(result.status.rebootRequired).toBe(true); + expect(result.status.statusDetail).toContain('restart'); + }); + + it('detects a reboot requirement from pending Windows optional feature state', async () => { + const execFileMock = ( + command: string, + args: string[], + _options: { + timeout: number; + windowsHide: boolean; + maxBuffer: number; + encoding: 'buffer'; + }, + callback: (error: Error | null, stdout: string | Buffer, stderr: string | Buffer) => void + ) => { + if (command === 'powershell.exe') { + callback( + null, + JSON.stringify([ + { + FeatureName: 'Microsoft-Windows-Subsystem-Linux', + State: 'EnablePending', + RestartRequired: 'Possible', + }, + { + FeatureName: 'VirtualMachinePlatform', + State: 'EnablePending', + RestartRequired: 'Possible', + }, + ]), + '' + ); + return; + } + + const key = args.join(' '); + if (key === '--status') { + callback(null, 'ok', ''); + return; + } + if (key === '--list --quiet') { + callback(null, '', ''); + return; + } + + callback( + Object.assign(new Error(`Unexpected command: ${command} ${key}`), { code: 'EFAIL' }), + '', + '' + ); + }; + + const service = new TmuxWslService(execFileMock, createPreferenceStore() as never); + + const result = await service.probe(); + + expect(result.status.wslInstalled).toBe(true); + expect(result.status.rebootRequired).toBe(true); + expect(result.status.statusDetail).toContain('restart'); + }); }); diff --git a/src/features/tmux-installer/main/infrastructure/wsl/__tests__/WindowsElevatedStepRunner.test.ts b/src/features/tmux-installer/main/infrastructure/wsl/__tests__/WindowsElevatedStepRunner.test.ts index 56c3019d..d939fe89 100644 --- a/src/features/tmux-installer/main/infrastructure/wsl/__tests__/WindowsElevatedStepRunner.test.ts +++ b/src/features/tmux-installer/main/infrastructure/wsl/__tests__/WindowsElevatedStepRunner.test.ts @@ -14,7 +14,18 @@ describe('WindowsElevatedStepRunner', () => { const resultFilePath = path.join(path.dirname(launcherScriptPath), 'result.json'); await fsp.writeFile( resultFilePath, - JSON.stringify({ ok: true, detail: 'WSL core installation command completed.' }), + JSON.stringify({ + ok: true, + detail: 'WSL core installation command completed.', + restartRequired: false, + featureStates: [ + { + featureName: 'Microsoft-Windows-Subsystem-Linux', + state: 'Enabled', + restartRequired: false, + }, + ], + }), 'utf8' ); callback(null, '', ''); @@ -26,6 +37,8 @@ describe('WindowsElevatedStepRunner', () => { expect(result.outcome).toBe('elevated_succeeded'); expect(result.detail).toContain('completed'); + expect(result.restartRequired).toBe(false); + expect(result.featureStates[0]?.featureName).toBe('Microsoft-Windows-Subsystem-Linux'); expect(result.resultFilePath).toContain('result.json'); }); @@ -36,7 +49,18 @@ describe('WindowsElevatedStepRunner', () => { const resultFilePath = path.join(path.dirname(launcherScriptPath), 'result.json'); await fsp.writeFile( resultFilePath, - `\uFEFF${JSON.stringify({ ok: true, detail: 'WSL core installation command completed.' })}`, + `\uFEFF${JSON.stringify({ + ok: true, + detail: 'WSL core installation command completed.', + restartRequired: true, + featureStates: [ + { + featureName: 'VirtualMachinePlatform', + state: 'EnablePending', + restartRequired: 'Possible', + }, + ], + })}`, 'utf8' ); callback(null, '', ''); @@ -48,6 +72,8 @@ describe('WindowsElevatedStepRunner', () => { expect(result.outcome).toBe('elevated_succeeded'); expect(result.detail).toContain('completed'); + expect(result.restartRequired).toBe(true); + expect(result.featureStates[0]?.state).toBe('EnablePending'); }); it('treats a missing result file plus cancel text as elevation cancellation', async () => { @@ -66,6 +92,8 @@ describe('WindowsElevatedStepRunner', () => { expect(result.outcome).toBe('elevated_cancelled'); expect(result.detail).toContain('cancelled'); + expect(result.restartRequired).toBe(false); + expect(result.featureStates).toEqual([]); expect(result.resultFilePath).toBeNull(); }); @@ -90,6 +118,7 @@ describe('WindowsElevatedStepRunner', () => { expect(result.outcome).toBe('elevated_unknown_outcome'); expect(result.detail).toContain('Требуемая операция выполнена успешно'); + expect(result.restartRequired).toBe(false); } finally { consoleWarnSpy.mockRestore(); } diff --git a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts index 32f11c61..1dd97aca 100644 --- a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts +++ b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts @@ -58,19 +58,19 @@ export class TmuxInstallerBannerAdapter { adapt(input: AdaptInput): TmuxInstallerBannerViewModel { const status = input.status; const snapshot = input.snapshot; + const hasActiveInstallFlow = + snapshot.phase !== 'idle' && snapshot.phase !== 'completed' && snapshot.phase !== 'cancelled'; + const tmuxMissing = status ? !status.effective.available : !input.loading; const visible = - snapshot.phase !== 'idle' || - (!input.loading && (status ? !status.effective.runtimeReady : true)); + hasActiveInstallFlow || (snapshot.phase !== 'completed' && !input.loading && tmuxMissing); const title = - snapshot.phase === 'idle' && status?.effective.available && !status.effective.runtimeReady - ? 'tmux needs one more step' - : snapshot.message && - (snapshot.phase === 'pending_external_elevation' || - snapshot.phase === 'waiting_for_external_step' || - snapshot.phase === 'needs_restart' || - snapshot.phase === 'needs_manual_step') - ? snapshot.message - : formatTmuxInstallerTitle(snapshot.phase); + snapshot.message && + (snapshot.phase === 'pending_external_elevation' || + snapshot.phase === 'waiting_for_external_step' || + snapshot.phase === 'needs_restart' || + snapshot.phase === 'needs_manual_step') + ? snapshot.message + : formatTmuxInstallerTitle(snapshot.phase); const primaryGuideUrl = status?.autoInstall.manualHints.find((hint) => typeof hint.url === 'string')?.url ?? null; const body = @@ -82,7 +82,7 @@ export class TmuxInstallerBannerAdapter { status?.wsl?.statusDetail ?? 'tmux improves persistent teammate reliability and cleaner recovery for long-running tasks.'; const benefitsBody = - status && !status.effective.runtimeReady ? formatTmuxOptionalBenefits(status.platform) : null; + status && !status.effective.available ? formatTmuxOptionalBenefits(status.platform) : null; const runtimeReadyLabel = status ? status.effective.runtimeReady ? 'Ready for persistent teammates' diff --git a/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts b/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts index c74d4758..2dbb9b7e 100644 --- a/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts +++ b/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts @@ -174,7 +174,7 @@ describe('TmuxInstallerBannerAdapter', () => { expect(result.showRefreshButton).toBe(true); }); - it('keeps the banner visible when tmux is installed but runtime is not ready yet', () => { + it('hides the banner when tmux is already installed', () => { const adapter = TmuxInstallerBannerAdapter.create(); const result = adapter.adapt({ @@ -196,12 +196,40 @@ describe('TmuxInstallerBannerAdapter', () => { detailsOpen: false, }); - expect(result.visible).toBe(true); - expect(result.title).toBe('tmux needs one more step'); + expect(result.visible).toBe(false); expect(result.locationLabel).toBe('Host runtime'); expect(result.runtimeReadyLabel).toBe('Installed, but not active yet'); expect(result.versionLabel).toBe('tmux 3.4'); - expect(result.benefitsBody).toContain('With tmux in WSL'); + expect(result.benefitsBody).toBeNull(); + }); + + it('hides a completed installer banner once tmux is available', () => { + const adapter = TmuxInstallerBannerAdapter.create(); + + const result = adapter.adapt({ + status: { + ...baseStatus, + effective: { + available: true, + location: 'host', + version: 'tmux 3.6a', + binaryPath: '/opt/homebrew/bin/tmux', + runtimeReady: true, + detail: 'tmux is available for persistent teammates.', + }, + }, + snapshot: { + ...idleSnapshot, + phase: 'completed', + strategy: 'homebrew', + message: 'tmux installed', + }, + loading: false, + error: null, + detailsOpen: false, + }); + + expect(result.visible).toBe(false); }); it('exposes installer input metadata for interactive privilege flows', () => {