mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
test(serve): observe delayed renderer containment
This commit is contained in:
@@ -2370,17 +2370,17 @@
|
||||
"layer": "electron-process-containment",
|
||||
"surfaces": ["headless orca serve", "offscreen browser page", "daemon-backed PTYs"],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "daemon"],
|
||||
"providers": ["daemon"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local", "daemon"],
|
||||
"coveredProviders": ["daemon"],
|
||||
"coverageNotes": "An isolated headless serve host creates two daemon-backed terminal sessions and one offscreen browser renderer. Exact WebContents and OS-process fencing prevents a fault from reaching an unrelated process. The oracle is host-local and does not claim paired-viewer continuity, renderer reload, systemd cgroup restart persistence, SSH or WSL coverage, or aggregate memory policy.",
|
||||
"motivatingLinks": [
|
||||
"https://linear.app/stably/issue/STA-5228",
|
||||
"https://github.com/stablyai/orca/issues/16084",
|
||||
"https://github.com/stablyai/orca/pull/16120"
|
||||
],
|
||||
"invariant": "Killing one exact offscreen Chromium renderer cannot replace or terminate the headless main process, runtime, daemon, or unrelated daemon-backed PTYs. The host session inventory retains each original tab-to-PTY binding, and both original PTY processes continue accepting input.",
|
||||
"oracle": "Start an isolated headless serve runtime with zero initial BrowserWindows, create two daemon-backed terminal fixtures, and record main, runtime, daemon, tab, PTY, fixture-process, WebContents, and renderer-process identities. Fence one marked offscreen page by WebContents, URL, and OS process, force that renderer to crash, and require its original process to die while main/runtime/daemon identities, both host tab-to-PTY bindings, and I/O through both original fixture processes remain unchanged.",
|
||||
"invariant": "Killing one exact offscreen Chromium renderer cannot replace or terminate the headless main process, runtime, daemon, or unrelated daemon-backed PTYs during the immediate fault or the reported nine-second incident interval. The host session inventory retains each original tab-to-PTY binding, and both original PTY processes continue accepting input.",
|
||||
"oracle": "Start an isolated headless serve runtime with zero initial BrowserWindows, create two daemon-backed terminal fixtures, and record main, runtime, daemon, tab, PTY, fixture-process, WebContents, and renderer-process identities. Fence one marked offscreen page by WebContents, URL, and OS process, force that renderer to crash, and require its original process to die. Observe beyond the reported nine-second renderer-victim-to-unit-failure interval while repeatedly requiring no main-process close, unchanged main/runtime/daemon identities, both host tab-to-PTY bindings, and I/O through both original fixture processes.",
|
||||
"commands": [
|
||||
"pnpm exec electron-vite build --mode e2e",
|
||||
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-serve-renderer-kill-retention.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
|
||||
@@ -2404,13 +2404,13 @@
|
||||
"platform": "macos",
|
||||
"command": "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-serve-renderer-kill-retention.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"result": "passed",
|
||||
"durationSeconds": 31,
|
||||
"summary": "The exact offscreen renderer process died while the headless main process, runtime, daemon, both original host tab-to-PTY bindings, and both original PTY processes remained live; both PTYs accepted post-fault input."
|
||||
"durationSeconds": 33,
|
||||
"summary": "The exact offscreen renderer process died while the headless main process, runtime, daemon, both original host tab-to-PTY bindings, and both original PTY processes remained live; repeated stability checkpoints beyond the reported nine-second interval retained post-fault I/O."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 60,
|
||||
"scope": "one isolated headless Electron host, one offscreen renderer fault, and two daemon-backed PTY fixtures"
|
||||
"scope": "one isolated headless Electron host, one offscreen renderer fault, two daemon-backed PTY fixtures, and a 12-second stability window"
|
||||
},
|
||||
"flakeHistory": {
|
||||
"status": "unknown",
|
||||
@@ -2418,7 +2418,7 @@
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "partial",
|
||||
"evidence": "The byte-identical oracle passes on reported v1.4.184 and current main, falsifying renderer-only death as the cause of runtime/session loss. The candidate adds characterization coverage only; reverting it removes the oracle without changing product behavior."
|
||||
"evidence": "The byte-identical oracle passes on reported v1.4.184 and current main: renderer-only death did not reproduce runtime or session loss in the isolated host topology. The process or event that ended the reported unit remains unidentified. The candidate adds characterization coverage only; reverting it removes the oracle without changing product behavior."
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": false,
|
||||
|
||||
@@ -25,6 +25,9 @@ type RendererIdentity = {
|
||||
webContentsId: number
|
||||
}
|
||||
|
||||
const INCIDENT_OBSERVATION_MS = 12_000
|
||||
const INCIDENT_CHECKPOINT_MS = 3_000
|
||||
|
||||
function readDaemonPid(userDataDir: string): number {
|
||||
const raw = readFileSync(
|
||||
path.join(userDataDir, 'daemon', `daemon-v${PROTOCOL_VERSION}.pid`),
|
||||
@@ -113,11 +116,20 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t
|
||||
fixturePath,
|
||||
path.join(scratch, 'pty-b.log')
|
||||
)
|
||||
await proveSameLivePty(call, terminalA, 'BEFORE_RENDERER_KILL_A')
|
||||
await proveSameLivePty(call, terminalB, 'BEFORE_RENDERER_KILL_B')
|
||||
const beforeInventory = await readHostTerminalInventory(call, worktreeId)
|
||||
expect(beforeInventory.ptyIdByTabId[terminalA.tabId]).toBe(terminalA.ptyId)
|
||||
expect(beforeInventory.ptyIdByTabId[terminalB.tabId]).toBe(terminalB.ptyId)
|
||||
const proveHostState = async (marker: string): Promise<void> => {
|
||||
const status = await launchedHost.client.call<RuntimeStatus>('status.get')
|
||||
expect(await launchedHost.app.evaluate(() => process.pid)).toBe(mainPid)
|
||||
expect(await processIdentityIsAlive(mainIdentity)).toBe(true)
|
||||
expect(status.result.runtimeId).toBe(beforeStatus.result.runtimeId)
|
||||
expect(readDaemonPid(userDataDir)).toBe(daemonPid)
|
||||
expect(await processIdentityIsAlive(daemonIdentity)).toBe(true)
|
||||
const inventory = await readHostTerminalInventory(call, worktreeId)
|
||||
expect(inventory.ptyIdByTabId[terminalA.tabId]).toBe(terminalA.ptyId)
|
||||
expect(inventory.ptyIdByTabId[terminalB.tabId]).toBe(terminalB.ptyId)
|
||||
await proveSameLivePty(call, terminalA, `${marker}_A`)
|
||||
await proveSameLivePty(call, terminalB, `${marker}_B`)
|
||||
}
|
||||
await proveHostState('BEFORE_RENDERER_KILL')
|
||||
|
||||
const pageId = `sta-5228-${Date.now()}`
|
||||
const markerUrl = `data:text/html,${encodeURIComponent(
|
||||
@@ -161,6 +173,10 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t
|
||||
'--type=renderer'
|
||||
)
|
||||
}
|
||||
let hostClosed = false
|
||||
launchedHost.app.once('close', () => {
|
||||
hostClosed = true
|
||||
})
|
||||
await launchedHost.app.evaluate(
|
||||
({ BrowserWindow }, expected) => {
|
||||
const win = BrowserWindow.getAllWindows().find(
|
||||
@@ -208,17 +224,17 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t
|
||||
})
|
||||
.toBe(false)
|
||||
|
||||
const afterStatus = await launchedHost.client.call<RuntimeStatus>('status.get')
|
||||
expect(await launchedHost.app.evaluate(() => process.pid)).toBe(mainPid)
|
||||
expect(await processIdentityIsAlive(mainIdentity)).toBe(true)
|
||||
expect(afterStatus.result.runtimeId).toBe(beforeStatus.result.runtimeId)
|
||||
expect(readDaemonPid(userDataDir)).toBe(daemonPid)
|
||||
expect(await processIdentityIsAlive(daemonIdentity)).toBe(true)
|
||||
await proveSameLivePty(call, terminalA, 'AFTER_RENDERER_KILL_A')
|
||||
await proveSameLivePty(call, terminalB, 'AFTER_RENDERER_KILL_B')
|
||||
const afterInventory = await readHostTerminalInventory(call, worktreeId)
|
||||
expect(afterInventory.ptyIdByTabId[terminalA.tabId]).toBe(terminalA.ptyId)
|
||||
expect(afterInventory.ptyIdByTabId[terminalB.tabId]).toBe(terminalB.ptyId)
|
||||
await proveHostState('AFTER_RENDERER_KILL')
|
||||
const observationDeadline = Date.now() + INCIDENT_OBSERVATION_MS
|
||||
let checkpoint = 0
|
||||
do {
|
||||
await new Promise((resolve) => setTimeout(resolve, INCIDENT_CHECKPOINT_MS))
|
||||
expect(
|
||||
hostClosed,
|
||||
'headless main exited during the incident-derived observation window'
|
||||
).toBe(false)
|
||||
await proveHostState(`STABILITY_${checkpoint++}`)
|
||||
} while (Date.now() < observationDeadline)
|
||||
|
||||
const afterRenderer = await readOffscreenRenderer(launchedHost.app, pageId, false)
|
||||
console.log('[STA-5228] renderer lifecycle snapshot', {
|
||||
@@ -227,7 +243,8 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t
|
||||
daemonPid,
|
||||
mainPid,
|
||||
ptyIds: [terminalA.ptyId, terminalB.ptyId],
|
||||
runtimeId: beforeStatus.result.runtimeId
|
||||
runtimeId: beforeStatus.result.runtimeId,
|
||||
stabilityCheckpoints: checkpoint
|
||||
})
|
||||
} finally {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user