mirror of
https://github.com/stablyai/orca.git
synced 2026-10-05 16:02:22 +00:00
test(preload): pin the one hop that keeps a malformed survival answer from skipping the quit warning
The rule that only an explicit yes is a yes had two expressions. The one in resolveLocalPtysSurviveQuit answers a typed in-process getter with a single production call site whose every path returns a boolean literal: instrumented across 35,022 tests it was evaluated 6 times, saw 'boolean' 6 times, and discriminated 0 times, and both deleting the check and swapping it for Boolean() reddened 0 of 115 while inverting it reddened 5. Deleted, since even a type violation there is caught downstream. The expression that survives is readWindowCloseRequestPayload, on the IPC hop where the answer really is unknown. Its own tests were green, but its only production call site was inline in the api object and no test in the tree imported it — forwarding the raw payload instead reddened 0 of 11,136. Extracted so the hop is reachable, and pinned: bypassing the reader now reddens 7, weakening it to Boolean() 5, acking after the callback 1, echoing a raw requestId 1, and dropping the unsubscribe 1.
This commit is contained in:
@@ -45,13 +45,19 @@ export function resolveWindowCloseAction(state: WindowCloseState): WindowCloseAc
|
||||
* Only a definite yes may skip the warning. A missing getter (window built before the
|
||||
* daemon wiring exists) or a throwing one is an undetermined answer, and an
|
||||
* undetermined answer is not a yes.
|
||||
*
|
||||
* Why no `=== true` on the getter's result: the answer is read here from a typed
|
||||
* in-process function, and it is read again from an `unknown` on the other side of
|
||||
* the IPC hop by `readWindowCloseRequestPayload`, which is where a shape that is
|
||||
* neither a clean yes nor a clean no can actually arrive. That rule lives once,
|
||||
* there; a second copy here answered nothing any producer could ask it.
|
||||
*/
|
||||
export function resolveLocalPtysSurviveQuit(getLocalPtysSurviveQuit?: () => boolean): boolean {
|
||||
if (!getLocalPtysSurviveQuit) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return getLocalPtysSurviveQuit() === true
|
||||
return getLocalPtysSurviveQuit()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
+4
-14
@@ -19,10 +19,8 @@ import {
|
||||
import type { DocPreviewGrantRequest } from './api/doc-preview-api'
|
||||
import type { AppIdentity } from '../shared/app-identity'
|
||||
import type { PtyProcessInspectionEvidence } from '../shared/pty-process-inspection-evidence'
|
||||
import {
|
||||
readWindowCloseRequestPayload,
|
||||
type WindowCloseRequestPayload
|
||||
} from '../shared/window-close-request'
|
||||
import type { WindowCloseRequestPayload } from '../shared/window-close-request'
|
||||
import { subscribeToWindowCloseRequest } from './window-close-request-subscription'
|
||||
import type { MacCapturedDigitRowChord } from '../shared/macos-symbolic-hotkeys'
|
||||
import type { ComputerAwakeStatus } from '../shared/computer-awake-mode'
|
||||
import type {
|
||||
@@ -4534,16 +4532,8 @@ const api = {
|
||||
/** Fired by main when the user tries to close the window; renderer confirms running
|
||||
* terminals then calls confirmWindowClose(). A quit (Cmd+Q / app.quit) skips that
|
||||
* dialog only when main also reports the local PTYs survive it. */
|
||||
onWindowCloseRequested: (callback: (data: WindowCloseRequestPayload) => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: unknown): void => {
|
||||
const payload = readWindowCloseRequestPayload(data)
|
||||
// Why: main cannot reach will-quit while a frozen renderer owns the window close handshake.
|
||||
ipcRenderer.send('window:close-request-received', payload.requestId)
|
||||
callback(payload)
|
||||
}
|
||||
ipcRenderer.on('window:close-requested', listener)
|
||||
return () => ipcRenderer.removeListener('window:close-requested', listener)
|
||||
},
|
||||
onWindowCloseRequested: (callback: (data: WindowCloseRequestPayload) => void): (() => void) =>
|
||||
subscribeToWindowCloseRequest(ipcRenderer, callback),
|
||||
/** Tell the main process to proceed with the window close. */
|
||||
confirmWindowClose: (): void => {
|
||||
ipcRenderer.send('window:confirm-close')
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { WindowCloseRequestPayload } from '../shared/window-close-request'
|
||||
import { subscribeToWindowCloseRequest } from './window-close-request-subscription'
|
||||
|
||||
type Listener = (event: unknown, data: unknown) => void
|
||||
|
||||
function installIpc(): {
|
||||
ipcRenderer: Parameters<typeof subscribeToWindowCloseRequest>[0]
|
||||
emit: (data: unknown) => void
|
||||
sent: unknown[][]
|
||||
removed: Listener[]
|
||||
} {
|
||||
const listeners: Listener[] = []
|
||||
const sent: unknown[][] = []
|
||||
const removed: Listener[] = []
|
||||
const ipcRenderer = {
|
||||
on: vi.fn((_channel: string, listener: Listener) => {
|
||||
listeners.push(listener)
|
||||
return ipcRenderer
|
||||
}),
|
||||
removeListener: vi.fn((_channel: string, listener: Listener) => {
|
||||
removed.push(listener)
|
||||
return ipcRenderer
|
||||
}),
|
||||
send: vi.fn((_channel: string, ...args: unknown[]) => {
|
||||
sent.push(args)
|
||||
})
|
||||
} as unknown as Parameters<typeof subscribeToWindowCloseRequest>[0]
|
||||
return {
|
||||
ipcRenderer,
|
||||
emit: (data: unknown) => {
|
||||
for (const listener of listeners) {
|
||||
listener({}, data)
|
||||
}
|
||||
},
|
||||
sent,
|
||||
removed
|
||||
}
|
||||
}
|
||||
|
||||
function deliver(data: unknown): {
|
||||
payload: WindowCloseRequestPayload | null
|
||||
sent: unknown[][]
|
||||
} {
|
||||
const { ipcRenderer, emit, sent } = installIpc()
|
||||
let payload: WindowCloseRequestPayload | null = null
|
||||
subscribeToWindowCloseRequest(ipcRenderer, (received) => {
|
||||
payload = received
|
||||
})
|
||||
emit(data)
|
||||
return { payload, sent }
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole quit-survival property rests on this hop: main's answer crosses IPC
|
||||
* untyped, and the renderer spends `localPtysSurviveQuit: true` by closing over
|
||||
* running terminals with no warning. Nothing else in the tree reads the payload,
|
||||
* so if the normalization is skipped here it is skipped everywhere — which is
|
||||
* exactly what happened while this listener was inline in index.ts and no test
|
||||
* could reach it.
|
||||
*/
|
||||
describe('window close request subscription', () => {
|
||||
it('delivers an explicit survival yes unchanged', () => {
|
||||
const { payload } = deliver({ isQuitting: true, localPtysSurviveQuit: true, requestId: 4 })
|
||||
|
||||
expect(payload).toEqual({ isQuitting: true, localPtysSurviveQuit: true, requestId: 4 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a truthy string', 'yes'],
|
||||
['a truthy number', 1],
|
||||
['an object', {}],
|
||||
['null', null]
|
||||
])('refuses to spend %s as a survival yes', (_label, value) => {
|
||||
const { payload } = deliver({ isQuitting: true, localPtysSurviveQuit: value })
|
||||
|
||||
expect(payload?.localPtysSurviveQuit).toBe(false)
|
||||
})
|
||||
|
||||
it('reads a payload with no survival field as "does not survive"', () => {
|
||||
const { payload } = deliver({ isQuitting: true })
|
||||
|
||||
expect(payload?.localPtysSurviveQuit).toBe(false)
|
||||
})
|
||||
|
||||
it('still answers when the payload is not an object at all', () => {
|
||||
const { payload, sent } = deliver(undefined)
|
||||
|
||||
expect(payload).toEqual({
|
||||
isQuitting: false,
|
||||
localPtysSurviveQuit: false,
|
||||
requestId: undefined
|
||||
})
|
||||
expect(sent).toEqual([[undefined]])
|
||||
})
|
||||
|
||||
// Why the ack and not just the payload: main clears its force-destroy timer on an
|
||||
// exact requestId match, so echoing a non-numeric one back would never match and a
|
||||
// frozen-renderer quit would sit on the timer instead of being acknowledged.
|
||||
it('acknowledges before the renderer runs, echoing only a numeric requestId', () => {
|
||||
const order: string[] = []
|
||||
const { ipcRenderer, emit, sent } = installIpc()
|
||||
vi.mocked(ipcRenderer.send).mockImplementation((_channel: string, ...args: unknown[]) => {
|
||||
order.push('ack')
|
||||
sent.push(args)
|
||||
})
|
||||
subscribeToWindowCloseRequest(ipcRenderer, () => order.push('callback'))
|
||||
|
||||
emit({ isQuitting: true, requestId: 9 })
|
||||
emit({ isQuitting: true, requestId: '9' })
|
||||
|
||||
expect(order).toEqual(['ack', 'callback', 'ack', 'callback'])
|
||||
expect(sent).toEqual([[9], [undefined]])
|
||||
})
|
||||
|
||||
it('unsubscribes the listener it registered', () => {
|
||||
const { ipcRenderer, removed } = installIpc()
|
||||
|
||||
const dispose = subscribeToWindowCloseRequest(ipcRenderer, () => {})
|
||||
dispose()
|
||||
|
||||
expect(removed).toHaveLength(1)
|
||||
expect(removed[0]).toBe(vi.mocked(ipcRenderer.on).mock.calls[0][1])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { IpcRenderer } from 'electron'
|
||||
import {
|
||||
readWindowCloseRequestPayload,
|
||||
type WindowCloseRequestPayload
|
||||
} from '../shared/window-close-request'
|
||||
|
||||
const CLOSE_REQUESTED_CHANNEL = 'window:close-requested'
|
||||
const CLOSE_REQUEST_RECEIVED_CHANNEL = 'window:close-request-received'
|
||||
|
||||
/**
|
||||
* Subscribes the renderer to main's `window:close-requested`.
|
||||
*
|
||||
* Why the payload is read here and not forwarded: this is the only boundary the
|
||||
* survival answer crosses untyped, and the renderer spends
|
||||
* `localPtysSurviveQuit: true` as permission to close over running work with no
|
||||
* warning. So the rule that only an explicit yes is a yes lives once, in
|
||||
* `readWindowCloseRequestPayload`, and this is its single call site — forwarding
|
||||
* the raw payload would restore the unconditional quit bypass for anything that
|
||||
* is not a clean boolean (docs/reference/ssh-execution-boundary.md).
|
||||
*
|
||||
* Extracted from the api object so that hop is reachable by a test at all; while
|
||||
* it was inline in index.ts nothing in the tree could observe it.
|
||||
*/
|
||||
export function subscribeToWindowCloseRequest(
|
||||
ipcRenderer: Pick<IpcRenderer, 'on' | 'removeListener' | 'send'>,
|
||||
callback: (data: WindowCloseRequestPayload) => void
|
||||
): () => void {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: unknown): void => {
|
||||
const payload = readWindowCloseRequestPayload(data)
|
||||
// Why: main cannot reach will-quit while a frozen renderer owns the window close handshake.
|
||||
ipcRenderer.send(CLOSE_REQUEST_RECEIVED_CHANNEL, payload.requestId)
|
||||
callback(payload)
|
||||
}
|
||||
ipcRenderer.on(CLOSE_REQUESTED_CHANNEL, listener)
|
||||
return () => ipcRenderer.removeListener(CLOSE_REQUESTED_CHANNEL, listener)
|
||||
}
|
||||
Reference in New Issue
Block a user