import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { Dialog, DialogContent } from '@/components/ui/dialog' import type { DesktopUpdateStatus } from '@/global' import { I18nProvider } from '@/i18n/context' import { $updateApply, $updateOverlayOpen, $updateOverlayTarget, $updateStatus, resetUpdateApplyState } from '@/store/updates' import { BlockerView, formatBlockerCommandLine, UpdatesOverlay } from './updates-overlay' async function renderWithI18n(ui: React.ReactNode) { await act(async () => { render( ({}), saveConfig: async () => ({ ok: true }) }}> {ui} ) }) } async function renderUpdatesOverlay() { await act(async () => { render( ({}), saveConfig: async () => ({ ok: true }) }}> ) }) } describe('formatBlockerCommandLine', () => { it('redacts the full ambiguous tail after a sensitive CLI, environment, or header marker', () => { const secretParts = ['first-part', 'second-part'] const secret = secretParts.join(' ') const commands = [ `python.exe watcher.py --token ${secret} --mode inspect`, ['python.exe watcher.py --password="', secret, '" --mode inspect'].join(''), `AUTH_TOKEN=${secret} python.exe watcher.py --mode inspect`, `python.exe watcher.py Authorization: Bearer ${secret} --mode inspect` ] for (const commandLine of commands) { const formatted = formatBlockerCommandLine(commandLine) expect(formatted).not.toContain(secretParts[0]) expect(formatted).not.toContain(secretParts[1]) expect(formatted).not.toContain('--mode inspect') expect(formatted.endsWith('[REDACTED]')).toBe(true) } }) it('redacts a sensitive query value and bounds the remaining diagnostic output', () => { const queryValue = ['private', 'value'].join('-') const commandLine = `python.exe watcher.py https://example.test/?api_key=${queryValue}&mode=inspect ` + 'x'.repeat(600) const formatted = formatBlockerCommandLine(commandLine) expect(formatted).not.toContain(queryValue) expect(formatted).toContain('api_key=[REDACTED]&mode=inspect') expect(Array.from(formatted).length).toBeLessThanOrEqual(500) expect(formatted.endsWith('…')).toBe(true) }) }) describe('BlockerView', () => { afterEach(() => { cleanup() $updateOverlayOpen.set(false) $updateOverlayTarget.set('client') $updateStatus.set(null) resetUpdateApplyState() }) it('uses the blocker view for a foreign process instead of the generic update error', async () => { $updateOverlayTarget.set('client') $updateOverlayOpen.set(true) $updateStatus.set({ supported: true, updateAvailable: true, behind: 1, commits: [] } as DesktopUpdateStatus) $updateApply.set({ applying: false, stage: 'error', message: 'Update aborted: another Hermes process is using this installation.', percent: null, error: 'venv-blocked', command: null, blockers: [ { pid: 58636, name: 'python.exe', cmdline: 'python.exe fenbi_session_refresh.py', kind: 'other', safeToStop: false } ], log: [] }) await renderUpdatesOverlay() expect(screen.getByText('Close other processes to update Hermes')).toBeTruthy() expect(screen.getByText('python.exe')).toBeTruthy() expect(screen.queryByText('Update didn’t finish')).toBeNull() }) it('identifies foreign blockers without offering automatic termination', async () => { const onStopAndUpdate = vi.fn() await renderWithI18n( {}} onStopAndUpdate={onStopAndUpdate} /> ) expect(screen.getByText('Close other processes to update Hermes')).toBeTruthy() expect(screen.getByText('python.exe')).toBeTruthy() expect(screen.getByText('PID 58636')).toBeTruthy() expect(screen.getByText(/can’t safely close these processes automatically/i)).toBeTruthy() expect(screen.getByText(/python.exe fenbi_session_refresh\.py/i)).toBeTruthy() expect(screen.queryByRole('button', { name: /close previews/i })).toBeNull() expect(onStopAndUpdate).not.toHaveBeenCalled() }) it('keeps mixed blocker cleanup limited to safe previews', async () => { const onStopAndUpdate = vi.fn() await renderWithI18n( {}} onStopAndUpdate={onStopAndUpdate} /> ) expect(screen.getByText(/can close the local previews listed below/i)).toBeTruthy() expect(screen.getByText('Example Preview')).toBeTruthy() expect(screen.getByText('python.exe')).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: 'Close previews and check again' })) expect(onStopAndUpdate).toHaveBeenCalledTimes(1) }) it('explains safe local previews and offers one-click close-and-update', async () => { const onStopAndUpdate = vi.fn() await renderWithI18n( {}} onStopAndUpdate={onStopAndUpdate} /> ) expect(screen.getByText('Close local previews to update Hermes?')).toBeTruthy() expect(screen.getByText('Example Preview')).toBeTruthy() expect(screen.getByText('Port 8766')).toBeTruthy() expect(screen.getByText(/will not modify or delete your files/i)).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: 'Close previews and update' })) expect(onStopAndUpdate).toHaveBeenCalledTimes(1) }) })