mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
Fix Settings menu click being silently dropped after a slow cold rendere
Replace the 60s pendingOpenSettings TTL with an untimed intent, since a cold renderer start can outrun any fixed timeout and cause the flag to expire before the click is consumed. webContents-id scoping plus consume-on-read still prevent the intent from leaking to an unrelated renderer. Adds a test covering the queued-before-mount pull path.
This commit is contained in:
+4
-3
@@ -817,9 +817,10 @@ function openSettingsFromSystemMenu(): void {
|
||||
// and leave a one-shot intent — a mounted renderer acts on the push, an
|
||||
// unmounted one pulls the intent at mount; only one fires per renderer life.
|
||||
targetWindow.webContents.send('ui:openSettings')
|
||||
// Why: 60s outlives the default flag TTL, which a cold renderer start can
|
||||
// exceed — an expired flag silently drops the Settings click.
|
||||
pendingOpenSettings.mark(targetWindow.webContents.id, 60_000)
|
||||
// Why: leave an untimed intent — any TTL can be outrun by a slow cold start and
|
||||
// would silently drop the Settings click. webContents-id scoping plus consume-on-
|
||||
// read still prevents the intent from leaking to a later, unrelated renderer.
|
||||
pendingOpenSettings.mark(targetWindow.webContents.id, Number.POSITIVE_INFINITY)
|
||||
}
|
||||
|
||||
function quitFromSystemTray(): void {
|
||||
|
||||
@@ -1300,6 +1300,130 @@ describe('useIpcEvents updater integration', () => {
|
||||
expect(removeSshCredentialRequest).toHaveBeenCalledWith('req-1')
|
||||
})
|
||||
|
||||
it('opens Settings from a Settings intent queued before the listener attached', async () => {
|
||||
const openSettingsPage = vi.fn()
|
||||
let onOpenSettingsRegistered = false
|
||||
|
||||
vi.doMock('react', async () => {
|
||||
const actual = await vi.importActual<typeof ReactModule>('react')
|
||||
return {
|
||||
...actual,
|
||||
useEffect: (effect: () => void | (() => void)) => {
|
||||
effect()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.doMock('../store', () => ({
|
||||
useAppStore: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
getState: () => ({
|
||||
openSettingsPage,
|
||||
fetchRepos: vi.fn(),
|
||||
fetchWorktrees: vi.fn(),
|
||||
setActiveView: vi.fn(),
|
||||
activeModal: null,
|
||||
closeModal: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
activeWorktreeId: 'wt-1',
|
||||
activeView: 'terminal',
|
||||
setActiveRepo: vi.fn(),
|
||||
setActiveWorktree: vi.fn(),
|
||||
revealWorktreeInSidebar: vi.fn(),
|
||||
setIsFullScreen: vi.fn(),
|
||||
setUpdateStatus: vi.fn(),
|
||||
setRateLimitsFromPush: vi.fn(),
|
||||
settings: { terminalFontSize: 13 }
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/ui-zoom', () => ({ applyUIZoom: vi.fn() }))
|
||||
vi.doMock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
ensureWorktreeHasInitialTerminal: vi.fn()
|
||||
}))
|
||||
vi.doMock('@/components/sidebar/visible-worktrees', () => ({
|
||||
getVisibleWorktreeIds: () => []
|
||||
}))
|
||||
vi.doMock('@/lib/editor-font-zoom', () => ({
|
||||
nextEditorFontZoomLevel: vi.fn(() => 0),
|
||||
computeEditorFontSize: vi.fn(() => 13)
|
||||
}))
|
||||
vi.doMock('@/components/settings/SettingsConstants', () => ({
|
||||
zoomLevelToPercent: vi.fn(() => 100),
|
||||
ZOOM_MIN: -3,
|
||||
ZOOM_MAX: 3
|
||||
}))
|
||||
vi.doMock('@/lib/zoom-events', () => ({ dispatchZoomLevelChanged: vi.fn() }))
|
||||
|
||||
const makeEvents = (target: Record<string, unknown> = {}): Record<string, unknown> =>
|
||||
new Proxy(target, {
|
||||
get: (namespace, prop) =>
|
||||
prop in namespace ? Reflect.get(namespace, prop) : () => () => {}
|
||||
})
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
repos: makeEvents(),
|
||||
worktrees: makeEvents(),
|
||||
keybindings: makeEvents(),
|
||||
settings: makeEvents(),
|
||||
browser: makeEvents(),
|
||||
updater: {
|
||||
getStatus: () => Promise.resolve({ state: 'idle' }),
|
||||
onStatus: () => () => {},
|
||||
onClearDismissal: () => () => {}
|
||||
},
|
||||
rateLimits: {
|
||||
get: () => Promise.resolve({ limits: {}, lastUpdatedAt: Date.now() }),
|
||||
onUpdate: () => () => {}
|
||||
},
|
||||
runtime: {
|
||||
getTerminalFitOverrides: () => Promise.resolve([]),
|
||||
getTerminalDrivers: () => Promise.resolve([]),
|
||||
getBrowserDrivers: () => Promise.resolve([]),
|
||||
onTerminalFitOverrideChanged: () => () => {},
|
||||
onTerminalDriverChanged: () => () => {},
|
||||
onBrowserDriverChanged: () => () => {}
|
||||
},
|
||||
ssh: {
|
||||
listTargets: () => Promise.resolve([]),
|
||||
listPortForwards: () => Promise.resolve([]),
|
||||
listDetectedPorts: () => Promise.resolve([]),
|
||||
getState: () => Promise.resolve(null),
|
||||
onStateChanged: () => () => {},
|
||||
onCredentialRequest: () => () => {},
|
||||
onCredentialResolved: () => () => {},
|
||||
onPortForwardsChanged: () => () => {},
|
||||
onDetectedPortsChanged: () => () => {}
|
||||
},
|
||||
agentStatus: { onSet: () => () => {} },
|
||||
ui: makeEvents({
|
||||
onOpenSettings: () => {
|
||||
onOpenSettingsRegistered = true
|
||||
return () => {}
|
||||
},
|
||||
// Why: exercise the positive branch — an intent queued before mount is
|
||||
// pulled once the renderer's onOpenSettings listener is attached.
|
||||
consumePendingOpenSettings: () => Promise.resolve(true),
|
||||
getZoomLevel: () => 0,
|
||||
set: vi.fn()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const { useIpcEvents } = await import('./useIpcEvents')
|
||||
|
||||
useIpcEvents()
|
||||
// Why: the pending-intent pull is a Promise; flush microtasks before asserting.
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(onOpenSettingsRegistered).toBe(true)
|
||||
expect(openSettingsPage).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('clears stale remote PTYs when an SSH connection fully disconnects', async () => {
|
||||
const clearTabPtyId = vi.fn()
|
||||
const setSshConnectionState = vi.fn()
|
||||
|
||||
Reference in New Issue
Block a user