orca/tests/e2e/dead-terminal-stress.spec.ts

141 lines
5.1 KiB
TypeScript

/**
* Aggressive stress tests for dead-terminal reproduction.
*
* These tests target specific failure vectors beyond the basic setup-split flow:
* - Forced WebGL context loss (simulating Chromium memory pressure)
* - Rapid switching during the ~200ms scheduleSplitScrollRestore window
*
* All tests require @headful mode for WebGL to be active.
*/
import { test, expect } from './helpers/orca-app'
import {
waitForSessionReady,
waitForActiveWorktree,
getActiveWorktreeId,
switchToWorktree,
ensureTerminalVisible
} from './helpers/store'
import { waitForActiveTerminalManager, waitForPaneCount } from './helpers/terminal'
import {
createAndActivateWorktreeWithSetup,
removeWorktreeViaStore,
waitForAllPanesToHaveContent
} from './helpers/dead-terminal'
const STRESS_ITERATIONS = 5
test.describe('Dead Terminal Stress @headful', () => {
const createdWorktreeIds: string[] = []
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await orcaPage.evaluate(async () => {
const state = window.__store?.getState()
if (!state) {
return
}
state.updateSettings({ setupScriptLaunchMode: 'split-vertical' })
})
})
test.afterEach(async ({ orcaPage }) => {
for (const id of createdWorktreeIds) {
await removeWorktreeViaStore(orcaPage, id)
}
createdWorktreeIds.length = 0
})
/**
* Force WebGL context loss on visible canvases immediately after a setup
* split. In production, Chromium reclaims WebGL contexts under memory
* pressure — especially with many worktrees open. The recovery path is:
* onContextLoss → dispose WebGL → DOM fallback → rAF → fit + refresh.
*/
test('@headful setup-split with forced WebGL context loss recovers', async ({ orcaPage }) => {
test.setTimeout(120_000)
const homeWorktreeId = await waitForActiveWorktree(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
for (let i = 0; i < STRESS_ITERATIONS; i++) {
const newId = await createAndActivateWorktreeWithSetup(orcaPage, `ctxloss-${i}`, 'vertical')
createdWorktreeIds.push(newId)
await expect.poll(async () => getActiveWorktreeId(orcaPage), { timeout: 10_000 }).toBe(newId)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await waitForPaneCount(orcaPage, 2, 15_000)
const lostCount = await orcaPage.evaluate(() => {
const canvases = document.querySelectorAll('.pane canvas:not(.xterm-link-layer)')
let lost = 0
for (const canvas of canvases) {
const gl =
(canvas as HTMLCanvasElement).getContext('webgl2') ??
(canvas as HTMLCanvasElement).getContext('webgl')
if (gl) {
const ext = gl.getExtension('WEBGL_lose_context')
if (ext) {
ext.loseContext()
lost++
}
}
}
return lost
})
if (lostCount > 0) {
console.log(`[ctxloss-${i}] Forced context loss on ${lostCount} canvases`)
}
await orcaPage.waitForTimeout(500)
await waitForAllPanesToHaveContent(orcaPage, `ctxloss-${i} after context loss`)
await switchToWorktree(orcaPage, homeWorktreeId)
await expect
.poll(async () => getActiveWorktreeId(orcaPage), { timeout: 10_000 })
.toBe(homeWorktreeId)
await removeWorktreeViaStore(orcaPage, newId)
createdWorktreeIds.pop()
}
})
/**
* Switch worktrees WITHOUT waiting for the split to settle. This hits the
* race between wrapInSplit() reparenting, WebGL context creation during
* resumeRendering(), and the scheduleSplitScrollRestore 200ms timer.
*/
test('@headful rapid worktree switching during setup-split lifecycle', async ({ orcaPage }) => {
test.setTimeout(120_000)
const homeWorktreeId = await waitForActiveWorktree(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
for (let i = 0; i < 3; i++) {
const newId = await createAndActivateWorktreeWithSetup(orcaPage, `rapid-${i}`, 'vertical')
createdWorktreeIds.push(newId)
// Switch away during the ~200ms scheduleSplitScrollRestore window
await orcaPage.waitForTimeout(50)
await switchToWorktree(orcaPage, homeWorktreeId)
await orcaPage.waitForTimeout(50)
// Switch back — triggers resumeRendering on partially-initialized panes
await switchToWorktree(orcaPage, newId)
await expect.poll(async () => getActiveWorktreeId(orcaPage), { timeout: 10_000 }).toBe(newId)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await waitForPaneCount(orcaPage, 2, 15_000)
await waitForAllPanesToHaveContent(orcaPage, `rapid-${i} after return`)
await switchToWorktree(orcaPage, homeWorktreeId)
await expect
.poll(async () => getActiveWorktreeId(orcaPage), { timeout: 10_000 })
.toBe(homeWorktreeId)
await removeWorktreeViaStore(orcaPage, newId)
createdWorktreeIds.pop()
}
})
})