Files
orca/src/main/window/createMainWindow-test-harness.ts
T
f20836c296 fix(recovery): prompt instead of reloading into a repeat Windows OOM when commit is exhausted (#23886)
* fix(recovery): ask instead of reloading into a repeat Windows OOM with exhausted commit

When another program exhausts Windows commit (RAM + page file), the renderer
OOMs, Orca auto-reloads 250 ms later, and the new renderer OOMs again within
seconds (launch 13084: 3.5 s after the reload; launch 22912: 34 s). The crash-loop
breaker (3 in 60 s) never opens for this cadence, so the user is never told the
machine is out of memory.

Keep the first automatic reload, but when a win32 reason=oom death follows
another OOM within 5 minutes and the pre-gone host sample shows under 512 MB of
available commit, escalate to the existing recovery prompt with a new
'low-commit' cause that names the MB left and suggests closing apps or growing
the page file. Records renderer_recovery_low_commit_prompt. No-op on
macOS/Linux and when commit is healthy.

* fix(recovery): gate low-commit prompt on post-OOM readings and recovered deaths only

- Reject pre-gone samples taken at or before the previous OOM; they miss the commit that corpse released.
- Record an OOM for the repeat window only once recovery actually runs, so skipped teardown OOMs cannot suppress the next first reload.
- Skip the install-ACL diagnosis on the low-commit prompt, whose text would not explain Copy Commands.

* fix(recovery): read commit at gone time when no sampler tick followed the previous OOM

The 10 s pre-gone sampler lands between ~3.5 s repeat OOMs only ~35% of the time, so the gate usually fell back to a silent reload. A gone-time read can only over-report free commit (the corpse already released its pages), so it can miss a prompt but never raise a false one.

* fix: reject invalid low-commit readings and clarify recovery advice

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 01:20:03 -07:00

192 lines
6.3 KiB
TypeScript

import { vi } from 'vitest'
import type { Mock } from 'vitest'
/** Loose spy signature: these stand in for electron APIs the suites only assert calls on. */
export type MainWindowSpy = Mock<(...args: unknown[]) => unknown>
export const menuPopupMock: MainWindowSpy = vi.fn()
export const notificationShowMock: MainWindowSpy = vi.fn()
/** Constructor spy: suites read the options object back off `mock.calls`. */
export const browserWindowMock: Mock<
(options: Electron.BrowserWindowConstructorOptions) => unknown
> = vi.fn()
export const openExternalMock: MainWindowSpy = vi.fn()
export const attachGuestPoliciesMock: MainWindowSpy = vi.fn()
export const attachRouteGuestMock: MainWindowSpy = vi.fn(() => false)
export const retireRouteRendererMock: MainWindowSpy = vi.fn()
export const attachClientPageRendererMock: MainWindowSpy = vi.fn()
export const retireClientPageRendererMock: MainWindowSpy = vi.fn()
export const buildFromTemplateMock: Mock<(...args: unknown[]) => { popup: MainWindowSpy }> = vi.fn(
() => ({ popup: menuPopupMock })
)
export const notificationMock: Mock<(...args: unknown[]) => { show: MainWindowSpy }> = vi.fn(
function () {
return { show: notificationShowMock }
}
)
export const powerMonitorOnMock: MainWindowSpy = vi.fn()
export const powerMonitorRemoveListenerMock: MainWindowSpy = vi.fn()
export const routePartitionAllowedMock: Mock<(partition: string) => boolean> = vi.fn(() => false)
export const isMock = { dev: false }
export const macosTahoeMock = { value: false }
type IpcMainMock = {
on: MainWindowSpy
removeListener: MainWindowSpy
handle: MainWindowSpy
removeHandler: MainWindowSpy
}
const ipcMainMock: IpcMainMock = {
on: vi.fn(),
removeListener: vi.fn(),
handle: vi.fn(),
removeHandler: vi.fn()
}
export type ElectronModuleMock = {
app: { on: MainWindowSpy; removeListener: MainWindowSpy }
BrowserWindow: typeof browserWindowMock
ipcMain: IpcMainMock
Menu: { buildFromTemplate: typeof buildFromTemplateMock }
Notification: typeof notificationMock
nativeTheme: { shouldUseDarkColors: boolean }
powerMonitor: { on: MainWindowSpy; removeListener: MainWindowSpy }
screen: {
getPrimaryDisplay: () => { workAreaSize: { width: number; height: number } }
getDisplayMatching: () => { scaleFactor: number }
}
shell: { openExternal: MainWindowSpy }
}
export type BrowserManagerModuleMock = {
browserManager: {
attachGuestPolicies: MainWindowSpy
setDictationShortcutForwardingPredicate: MainWindowSpy
}
}
// Why: vi.mock factories are hoisted per test file, so each file calls these builders instead of
// re-declaring the shared electron surface.
export function electronModuleMock(): ElectronModuleMock {
return {
app: { on: vi.fn(), removeListener: vi.fn() },
BrowserWindow: browserWindowMock,
ipcMain: ipcMainMock,
Menu: { buildFromTemplate: buildFromTemplateMock },
Notification: notificationMock,
nativeTheme: { shouldUseDarkColors: false },
powerMonitor: { on: powerMonitorOnMock, removeListener: powerMonitorRemoveListenerMock },
screen: {
getPrimaryDisplay: () => ({ workAreaSize: { width: 1440, height: 900 } }),
getDisplayMatching: () => ({ scaleFactor: 2 })
},
shell: { openExternal: openExternalMock }
}
}
export function electronToolkitUtilsMock() {
return {
is: isMock
}
}
export function macosTahoeReleaseMock() {
return {
isMacosTahoeOrNewer: vi.fn(() => macosTahoeMock.value)
}
}
export function appIconMock() {
return {
getAppIconPath: vi.fn(() => 'icon')
}
}
export function browserManagerMock(): BrowserManagerModuleMock {
return {
browserManager: {
attachGuestPolicies: attachGuestPoliciesMock,
setDictationShortcutForwardingPredicate: vi.fn()
}
}
}
export function resetMainWindowMocks(): void {
browserWindowMock.mockReset()
openExternalMock.mockReset()
attachGuestPoliciesMock.mockReset()
attachRouteGuestMock.mockReset()
attachRouteGuestMock.mockReturnValue(false)
retireRouteRendererMock.mockReset()
attachClientPageRendererMock.mockReset()
retireClientPageRendererMock.mockReset()
buildFromTemplateMock.mockClear()
menuPopupMock.mockClear()
notificationMock.mockClear()
notificationShowMock.mockClear()
powerMonitorOnMock.mockReset()
powerMonitorRemoveListenerMock.mockReset()
routePartitionAllowedMock.mockReset()
routePartitionAllowedMock.mockReturnValue(false)
isMock.dev = false
macosTahoeMock.value = false
ipcMainMock.on.mockReset()
ipcMainMock.removeListener.mockReset()
ipcMainMock.handle.mockReset()
ipcMainMock.removeHandler.mockReset()
}
export function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
try {
return run()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: original })
}
}
export type RendererRecoveryWindowHarness = {
browserWindowInstance: { loadFile: Mock<() => Promise<void>>; loadURL: Mock<() => Promise<void>> }
windowHandlers: Record<string, (...args: any[]) => void>
}
/** A main window whose webContents events land in `windowHandlers` for suites that drive renderer recovery. */
export function createRendererRecoveryWindowHarness(): RendererRecoveryWindowHarness {
const windowHandlers: RendererRecoveryWindowHarness['windowHandlers'] = {}
const webContents = {
id: 143,
getURL: vi.fn(() => 'file:///opt/orca/renderer/index.html'),
isDestroyed: vi.fn(() => false),
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
windowHandlers[event] = handler
}),
setZoomLevel: vi.fn(),
setBackgroundThrottling: vi.fn(),
invalidate: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn()
}
const browserWindowInstance = {
webContents,
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
windowHandlers[event] = handler
}),
isDestroyed: vi.fn(() => false),
isMaximized: vi.fn(() => true),
isFullScreen: vi.fn(() => false),
getSize: vi.fn(() => [1200, 800]),
setSize: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(() => Promise.resolve()),
loadURL: vi.fn(() => Promise.resolve())
}
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
return { browserWindowInstance, windowHandlers }
}