mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 08:02:32 +00:00
* 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>
192 lines
6.3 KiB
TypeScript
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 }
|
|
}
|