Files
aiturk-hermes-ide/apps/desktop/src/app/updates-overlay.blockers.test.tsx
T

214 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
<I18nProvider configClient={{ getConfig: async () => ({}), saveConfig: async () => ({ ok: true }) }}>
<Dialog open>
<DialogContent>{ui}</DialogContent>
</Dialog>
</I18nProvider>
)
})
}
async function renderUpdatesOverlay() {
await act(async () => {
render(
<I18nProvider configClient={{ getConfig: async () => ({}), saveConfig: async () => ({ ok: true }) }}>
<UpdatesOverlay />
</I18nProvider>
)
})
}
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(
<BlockerView
blockers={[
{
pid: 58636,
name: 'python.exe',
cmdline: 'python.exe fenbi_session_refresh.py',
kind: 'other',
safeToStop: false
}
]}
onDismiss={() => {}}
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(
<BlockerView
blockers={[
{
pid: 47484,
name: 'python.exe',
cmdline: 'python.exe -m http.server 8766',
kind: 'local-preview',
safeToStop: true,
label: 'Example Preview',
port: 8766
},
{
pid: 58636,
name: 'python.exe',
cmdline: 'python.exe fenbi_session_refresh.py',
kind: 'other',
safeToStop: false
}
]}
onDismiss={() => {}}
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(
<BlockerView
blockers={[
{
pid: 47484,
name: 'python.exe',
cmdline: 'python.exe -m http.server 8766',
kind: 'local-preview',
safeToStop: true,
label: 'Example Preview',
port: 8766
}
]}
onDismiss={() => {}}
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)
})
})