test(desktop): import settings panels once so first-test imports can't time out

Several settings suites imported their component inside each test body.
The first in-test `await import` paid the whole jsdom env init + module
transform + import graph inside test one's 15s budget, which times out
under CI runner load; the late-finishing import could also leak its first
render into the next test ("multiple Honcho settings buttons").

Load each panel once at module scope (same pattern as
profile-scope.test.tsx / config-settings.test.tsx), where the cost is
outside every test timeout. No behavior change: hoisted vi.mock factories
still intercept these imports identically.

Seen on #122304, #122269, #122368, #121523.
This commit is contained in:
Hermes Agent
2026-09-25 11:28:56 -05:00
committed by brooklyn!
parent ba1c129692
commit 722a02f967
7 changed files with 42 additions and 44 deletions

View File

@@ -37,6 +37,12 @@ vi.mock('@/store/notifications', () => ({
notifyError: (...args: unknown[]) => notifyError(...args)
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
const { $activeGatewayProfile, $profiles } = await import('@/store/profile')
const { $settingsScopeOverride } = await import('@/store/settings-scope')
const emptyResponse: CustomEndpointsResponse = {
current: { base_url: '', model: '', provider: '' },
endpoints: []
@@ -60,18 +66,15 @@ const savedResponse: CustomEndpointsResponse = {
ok: true
}
beforeEach(async () => {
const { $activeGatewayProfile, $profiles } = await import('@/store/profile')
const { $settingsScopeOverride } = await import('@/store/settings-scope')
beforeEach(() => {
$activeGatewayProfile.set('default')
$settingsScopeOverride.set(null)
$profiles.set([])
})
afterEach(async () => {
afterEach(() => {
cleanup()
vi.clearAllMocks()
const { $settingsScopeOverride } = await import('@/store/settings-scope')
$settingsScopeOverride.set(null)
})
@@ -79,7 +82,6 @@ describe('CustomEndpointsSettings', () => {
it('localizes endpoint editing on language changes without changing transport or draft identifiers', async () => {
getCustomEndpoints.mockResolvedValue(emptyResponse)
saveCustomEndpoint.mockResolvedValue(savedResponse)
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
let language!: I18nContextValue
function Surface() {
@@ -129,7 +131,6 @@ describe('CustomEndpointsSettings', () => {
transport_checked: 'codex_responses'
})
saveCustomEndpoint.mockResolvedValue(savedResponse)
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
render(<CustomEndpointsSettings />)
@@ -164,8 +165,6 @@ describe('CustomEndpointsSettings', () => {
})
it('loads and saves endpoints for the Settings Applies-to profile, not only the active bot', async () => {
const { $activeGatewayProfile, $profiles } = await import('@/store/profile')
const { $settingsScopeOverride } = await import('@/store/settings-scope')
$activeGatewayProfile.set('carousel-director')
$settingsScopeOverride.set('content-studio')
$profiles.set(
@@ -181,7 +180,6 @@ describe('CustomEndpointsSettings', () => {
)
getCustomEndpoints.mockResolvedValue(emptyResponse)
saveCustomEndpoint.mockResolvedValue(savedResponse)
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
render(<CustomEndpointsSettings />)
@@ -206,7 +204,6 @@ describe('CustomEndpointsSettings', () => {
...savedResponse,
endpoints: [{ ...savedResponse.endpoints[0], api_mode: 'anthropic_messages' }]
})
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
render(<CustomEndpointsSettings />)
@@ -220,7 +217,6 @@ describe('CustomEndpointsSettings', () => {
getCustomEndpoints.mockResolvedValue(emptyResponse)
const onConfigSaved = vi.fn()
const onMainModelChanged = vi.fn()
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
const view = render(
<CustomEndpointsSettings onConfigSaved={onConfigSaved} onMainModelChanged={onMainModelChanged} />
@@ -255,7 +251,6 @@ describe('CustomEndpointsSettings', () => {
models: ['model-a'],
resolved_base_url: 'http://h.test/v1'
})
const { CustomEndpointsSettings } = await import('./custom-endpoints-settings')
render(<CustomEndpointsSettings onConfigSaved={vi.fn()} onMainModelChanged={vi.fn()} />)
await screen.findByText('No custom endpoints')

View File

@@ -15,6 +15,10 @@ vi.mock('@/hermes', () => ({
getGlobalModelOptions: () => getGlobalModelOptions()
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { FallbackModelsField } = await import('./fallback-models-field')
beforeEach(() => {
getGlobalModelOptions.mockResolvedValue({
providers: [
@@ -30,8 +34,7 @@ afterEach(() => {
vi.clearAllMocks()
})
async function renderField(value: unknown, onChange = vi.fn()) {
const { FallbackModelsField } = await import('./fallback-models-field')
function renderField(value: unknown, onChange = vi.fn()) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
render(
@@ -43,8 +46,7 @@ async function renderField(value: unknown, onChange = vi.fn()) {
return onChange
}
async function renderFieldWithRerender(value: unknown, onChange = vi.fn()) {
const { FallbackModelsField } = await import('./fallback-models-field')
function renderFieldWithRerender(value: unknown, onChange = vi.fn()) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const view = render(

View File

@@ -18,6 +18,11 @@ vi.mock('@/hermes', () => ({
setEnvVar: vi.fn()
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { KeysSettings } = await import('./keys-settings')
const { $settingsScopeOverride } = await import('@/store/settings-scope')
beforeEach(() => {
getEnvVars.mockResolvedValue({})
Object.defineProperty(Element.prototype, 'scrollIntoView', {
@@ -32,8 +37,6 @@ afterEach(() => {
})
async function renderKeysSettings(view: 'settings' | 'tools', route = '/settings') {
const { KeysSettings } = await import('./keys-settings')
await act(async () => {
render(
<MemoryRouter initialEntries={[route]}>
@@ -111,8 +114,6 @@ describe('KeysSettings', () => {
FIRECRAWL_API_KEY: envVar('tool', { description: 'Crawl and extract websites.' })
})
const { KeysSettings } = await import('./keys-settings')
render(
<MemoryRouter initialEntries={['/settings?tab=keys']}>
<KeysSettings view="tools" />
@@ -135,16 +136,12 @@ describe('KeysSettings', () => {
// changes, but the in-flight edit map was not reset with it. A value typed
// while targeting profile-b survived the switch to profile-c, where the
// still-live Save would persist it into the WRONG profile.
const { $settingsScopeOverride } = await import('@/store/settings-scope')
$settingsScopeOverride.set('profile-b')
getEnvVars.mockResolvedValue({
WIDGET_API_KEY: envVar('tool', { description: 'Widget key.', is_set: true, redacted_value: '••••••' })
})
try {
const { KeysSettings } = await import('./keys-settings')
const { container } = render(
<MemoryRouter initialEntries={['/settings']}>
<KeysSettings view="tools" />

View File

@@ -20,6 +20,10 @@ vi.mock('@/store/notifications', () => ({
notifyError: vi.fn()
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { ProviderConfigModal } = await import('./provider-config-modal')
function field(
overrides: Partial<MemoryProviderField> & Pick<MemoryProviderField, 'key' | 'kind'>
): MemoryProviderField {
@@ -65,8 +69,7 @@ afterEach(() => {
vi.clearAllMocks()
})
async function renderModal(open = true) {
const { ProviderConfigModal } = await import('./provider-config-modal')
function renderModal(open = true) {
const onOpenChange = vi.fn()
const onSaved = vi.fn().mockResolvedValue(undefined)

View File

@@ -22,6 +22,13 @@ vi.mock('@/store/notifications', () => ({
notifyError: vi.fn()
}))
// Load the panel once at module scope, outside every test timeout. The first
// in-test `await import` paid the whole transform + import inside test one's
// 15s budget — the recurring CI timeout — and the late-finishing import could
// leak its first render into the next test ("multiple Honcho settings
// buttons").
const { ProviderConfigPanel } = await import('./provider-config-panel')
function honchoSchema(): MemoryProviderConfig {
return {
name: 'honcho',
@@ -108,9 +115,7 @@ afterEach(() => {
vi.clearAllMocks()
})
async function renderPanel(provider = 'honcho') {
const { ProviderConfigPanel } = await import('./provider-config-panel')
function renderPanel(provider = 'honcho') {
return render(<ProviderConfigPanel provider={provider} />)
}

View File

@@ -37,6 +37,12 @@ vi.mock('@/store/onboarding', () => ({
startManualLocalEndpoint: (reason: null | string) => startManualLocalEndpoint(reason)
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { ProvidersSettings } = await import('./providers-settings')
const { $settingsScopeOverride } = await import('@/store/settings-scope')
const { $activeGatewayProfile, $profiles } = await import('@/store/profile')
function provider(id: string, loggedIn: boolean, patch: Partial<OAuthProvider> = {}): OAuthProvider {
return {
cli_command: `hermes auth add ${id}`,
@@ -90,7 +96,6 @@ afterEach(() => {
// Removal goes through confirm() from @/store/confirm, so the host has to be
// mounted for the prompt to render — same as in the real app shell.
async function renderProvidersSettings() {
const { ProvidersSettings } = await import('./providers-settings')
let result: ReturnType<typeof render>
await act(async () => {
result = render(
@@ -106,8 +111,6 @@ async function renderProvidersSettings() {
describe('ProvidersSettings', () => {
it('reads and saves API keys for the shared Settings target and reloads when it changes', async () => {
const { $settingsScopeOverride } = await import('@/store/settings-scope')
const { $activeGatewayProfile, $profiles } = await import('@/store/profile')
$activeGatewayProfile.set('profile-a')
$settingsScopeOverride.set('profile-b')
$profiles.set(
@@ -122,8 +125,6 @@ describe('ProvidersSettings', () => {
}))
)
getEnvVars.mockResolvedValue({ WIDGET_API_KEY: keyVar({ provider: 'widget', provider_label: 'Widget' }) })
const { ProvidersSettings } = await import('./providers-settings')
try {
const { container } = render(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
await screen.findByText('Widget')
@@ -147,7 +148,6 @@ describe('ProvidersSettings', () => {
})
it('uses the settings target for account reads, removal and sign-in', async () => {
const { $settingsScopeOverride } = await import('@/store/settings-scope')
$settingsScopeOverride.set('beta')
try {
@@ -231,7 +231,6 @@ describe('ProvidersSettings', () => {
})
listOAuthProviders.mockResolvedValue({ providers: [] })
const { ProvidersSettings } = await import('./providers-settings')
await act(async () => {
render(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
})
@@ -250,7 +249,6 @@ describe('ProvidersSettings', () => {
})
listOAuthProviders.mockResolvedValue({ providers: [] })
const { ProvidersSettings } = await import('./providers-settings')
render(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
// Equal priority → alphabetical tiebreak: Acme, Middle, Zebra.
@@ -283,7 +281,6 @@ describe('ProvidersSettings', () => {
getEnvVars.mockResolvedValue({})
listOAuthProviders.mockResolvedValue({ providers: [] })
const { ProvidersSettings } = await import('./providers-settings')
render(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
const row = await screen.findByText('Local / custom endpoint')

View File

@@ -22,6 +22,10 @@ vi.mock('@/store/notifications', () => ({
notifyError: vi.fn()
}))
// Load once at module scope so no test's 15s budget pays the heavy transform
// + import (the first-test timeout flake under CI load).
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
function backends(overrides: Partial<TerminalBackendsResponse> = {}): TerminalBackendsResponse {
return {
active: 'local',
@@ -68,7 +72,6 @@ afterEach(() => {
describe('TerminalBackendPanel', () => {
it('marks the active backend as pressed', async () => {
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
render(<TerminalBackendPanel onConfiguredChange={vi.fn()} />)
const local = await screen.findByRole('button', { name: /Local/ })
@@ -77,7 +80,6 @@ describe('TerminalBackendPanel', () => {
it('selects a backend when clicked and reports the change', async () => {
const onConfiguredChange = vi.fn()
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
render(<TerminalBackendPanel onConfiguredChange={onConfiguredChange} />)
fireEvent.click(await screen.findByRole('button', { name: /SSH/ }))
@@ -93,7 +95,6 @@ describe('TerminalBackendPanel', () => {
const confirmGate = deferred<boolean>()
confirmMock.mockReturnValue(confirmGate.promise)
selectTerminalBackend.mockResolvedValue({ ok: true, backend: 'docker' })
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
render(<TerminalBackendPanel onConfiguredChange={vi.fn()} />)
fireEvent.click(await screen.findByRole('button', { name: /Docker/ }))
@@ -118,7 +119,6 @@ describe('TerminalBackendPanel', () => {
it('does not select a needs_setup backend when the confirm dialog is declined', async () => {
const confirmGate = deferred<boolean>()
confirmMock.mockReturnValue(confirmGate.promise)
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
render(<TerminalBackendPanel onConfiguredChange={vi.fn()} />)
fireEvent.click(await screen.findByRole('button', { name: /Docker/ }))
@@ -137,7 +137,6 @@ describe('TerminalBackendPanel', () => {
})
it('does not re-select the already active backend', async () => {
const { TerminalBackendPanel } = await import('./terminal-backend-panel')
render(<TerminalBackendPanel onConfiguredChange={vi.fn()} />)
fireEvent.click(await screen.findByRole('button', { name: /Local/ }))