fix(e2e): third pass — harden the last 4 chronic/flaky e2e gates

- agent-session-live-force-exit-resume: raise persisted-record poll 15s→30s
  (two-stage debounced write + main scheduleSave needs headroom under the CI
  event-loop starvation that also drifts renderer timers ~1s in this shard);
  on miss, dump store vs disk state to distinguish a lost write from slow flush.
- artificial-opencode-terminal-load: add MAX_TIMER_DRIFT_UNDER_LOAD_MS (2.5s)
  for the injected-load scenarios, mirroring MAX_WORST_KEY_LATENCY_UNDER_LOAD_MS;
  baseline single-terminal gate stays at 250ms.
- combined-diff-scroll-restore: converge the after-tab-switch anchor via bounded
  retry (Monaco restores scroll over several layout passes) before asserting;
  a genuine restore miss still fails since the last anchor is returned on timeout.
- terminal-reattach-mouse-mode-leak: poll rAFs until the enable-mouse-events
  class lands after re-arming instead of a single frame (batched xterm render).

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-13 00:00:56 -07:00
co-authored by Orca
parent 2d15a856ae
commit 3e1c56c72e
4 changed files with 69 additions and 12 deletions
@@ -211,12 +211,32 @@ test('resumes a live agent record after force-exit restart when pane PTY ownersh
// hydration-gated writer before polling persisted state.
await page.evaluate(() => window.__store?.getState().captureAllSleepingAgentSessions())
await expect
.poll(() => persistedLiveRecordExists(session.userDataDir), {
timeout: 15_000,
message: 'Live sleeping-agent record was not persisted before force exit'
})
.toBe(true)
// Why: the record reaches disk via the debounced session writer (150ms) plus
// the main-process scheduleSave (up to 5s). Under CI event-loop starvation —
// the same shard drifts renderer timers ~1s — both stages need headroom, so
// poll to 30s (this suite's other readiness budget). On a miss, surface store
// vs disk state to separate a lost write from a merely slow flush.
const persistDeadline = Date.now() + 30_000
let persisted = false
while (Date.now() < persistDeadline) {
if (persistedLiveRecordExists(session.userDataDir)) {
persisted = true
break
}
await page.waitForTimeout(250)
}
if (!persisted) {
const storeRecords = await page.evaluate(
() => window.__store?.getState().sleepingAgentSessionsByPaneKey
)
throw new Error(
`Live sleeping-agent record was not persisted before force exit. store=${JSON.stringify(
storeRecords
)} disk=${JSON.stringify(
readPersistedData(session.userDataDir).workspaceSession?.sleepingAgentSessionsByPaneKey
)}`
)
}
const daemonPid = readDaemonPid(session.userDataDir)
await forceKillElectronApp(firstApp)
@@ -139,6 +139,12 @@ const MAX_REVISIT_LATENCY_UNDER_LOAD_MS = 3_000
// Why: GitHub's two-worker Electron shards can briefly starve renderer timers
// without visible typing lag. Keep this as a smoke gate, not a CPU lottery.
const MAX_TIMER_DRIFT_MS = 250
// Why: under injected multi-pane redraw load the renderer event loop is
// environment-dominated (seen at ~1s on a CPU-starved OSS shard) even when
// typing stays responsive, mirroring MAX_WORST_KEY_LATENCY_UNDER_LOAD_MS. Keep
// this only as a catastrophic-starvation gate; the unloaded 250ms budget guards
// the real baseline.
const MAX_TIMER_DRIFT_UNDER_LOAD_MS = 2_500
const MAX_SCROLL_LATENCY_MS = 150
const MAX_RENDERER_SCHEDULER_QUEUED_CHARS = 3 * 1024 * 1024
@@ -446,7 +452,7 @@ async function measureCrossWorkspaceTypingDuringHiddenLoad({
expect(scheduler?.rendererDroppedBacklogs ?? 0).toBe(0)
expect(measurement.medianLatencyMs).toBeLessThan(MAX_MEDIAN_KEY_LATENCY_MS)
expect(measurement.worstLatencyMs).toBeLessThan(MAX_WORST_KEY_LATENCY_UNDER_LOAD_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_UNDER_LOAD_MS)
} finally {
await load.stop()
await sendToTerminal(orcaPage, typingPtyId, '\x03').catch(() => undefined)
@@ -578,7 +584,7 @@ test.describe('Artificial OpenCode terminal load', () => {
)
expect(measurement.medianLatencyMs).toBeLessThan(MAX_MEDIAN_KEY_LATENCY_MS)
expect(measurement.worstLatencyMs).toBeLessThan(MAX_WORST_KEY_LATENCY_UNDER_LOAD_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_UNDER_LOAD_MS)
} finally {
await load.stop()
await sendToTerminal(orcaPage, typingPane.ptyId, '\x03').catch(() => undefined)
@@ -673,7 +679,7 @@ test.describe('Artificial OpenCode terminal load', () => {
)
expect(measurement.medianLatencyMs).toBeLessThan(MAX_MEDIAN_KEY_LATENCY_MS)
expect(measurement.worstLatencyMs).toBeLessThan(MAX_WORST_KEY_LATENCY_UNDER_LOAD_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_MS)
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_TIMER_DRIFT_UNDER_LOAD_MS)
} finally {
await load.stop()
await sendToTerminal(orcaPage, typingPane.ptyId, '\x03').catch(() => undefined)
+26 -1
View File
@@ -241,6 +241,31 @@ async function waitForStableViewportAnchor(page: Page): Promise<ViewportAnchor>
throw new Error(`combined diff viewport anchor did not settle: ${JSON.stringify(lastAnchor)}`)
}
// Why: remounting the diff on tab switch restores scroll in several Monaco
// layout passes, so the first settled anchor can be a mid-restore frame. Poll
// until the anchor converges near the pre-switch offset before asserting; a
// genuine restore miss still surfaces because the last anchor is returned on
// timeout for the caller's assertion to fail on.
async function waitForRestoredViewportAnchor(
page: Page,
target: ViewportAnchor,
tolerancePx = 80
): Promise<ViewportAnchor> {
const startedAt = Date.now()
let lastAnchor = await waitForStableViewportAnchor(page)
while (Date.now() - startedAt < 10_000) {
if (lastAnchor.key === target.key && Math.abs(lastAnchor.top - target.top) < tolerancePx) {
return lastAnchor
}
await page.waitForTimeout(100)
const anchor = await readViewportAnchor(page)
if (anchor) {
lastAnchor = anchor
}
}
return lastAnchor
}
async function startCombinedDiffScrollProbe(page: Page): Promise<void> {
await page.evaluate(() => {
type CombinedDiffScrollProbe = {
@@ -415,7 +440,7 @@ test.describe('Combined diff scroll restore', () => {
await orcaPage.locator(`[data-tab-id="${diffTabId}"]`).click({ force: true })
await expect(orcaPage.locator('.combined-diff-scroll-container')).toBeVisible()
const afterSwitch = await waitForStableViewportAnchor(orcaPage)
const afterSwitch = await waitForRestoredViewportAnchor(orcaPage, beforeSwitch)
expect(afterSwitch.key).toBe(beforeSwitch.key)
expect(Math.abs(afterSwitch.top - beforeSwitch.top)).toBeLessThan(80)
@@ -230,8 +230,14 @@ test.describe('reattach mouse-mode leak', () => {
await new Promise<void>((resolve) =>
pane.terminal.write('\x1b[?1003h\x1b[?1006h', () => resolve())
)
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)))
const classAfterArm = pane.terminal.element.classList.contains('enable-mouse-events')
// Why: xterm toggles the enable-mouse-events class from a batched
// render pass, so a single rAF can race it on a slow CI shard; poll a
// few frames until the class lands before probing motion.
let classAfterArm = false
for (let attempt = 0; attempt < 30 && !classAfterArm; attempt += 1) {
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)))
classAfterArm = pane.terminal.element.classList.contains('enable-mouse-events')
}
await dispatchMotion()
return { afterReattach, classAfterArm, armedReports: motionReports().length }