test(e2e): stabilize terminal failure coverage

This commit is contained in:
Jinjing
2026-08-13 23:27:57 -07:00
parent b19d99bd00
commit 8ab035102e
5 changed files with 80 additions and 26 deletions
+35
View File
@@ -0,0 +1,35 @@
# E2E run 31749134781 triage
Run: [E2E attempt 1](https://github.com/stablyai/orca/actions/runs/31749134781/attempts/1), scheduled on `main` at `e06cdbb4ee0442599eaa7ecef759b8a600bb2cf5` (2026-08-13). Three of ten shards failed; each had one failing test. The other seven shards and Docker SSH isolation passed.
## `e2e 3-of-10`: terminal link tooltip
- **Test:** `tests/e2e/issue-12656-terminal-link-tooltip.spec.ts:121`, `clears hover state without permanently shrinking the terminal`.
- **Evidence:** after 10 seconds repeatedly moving to the printed URL, the expected `{ display: '', currentLinkText: '<url>' }` remained `{ display: 'none', currentLinkText: null }` at line 154. The terminal geometry assertion immediately before the hover passed.
- **Likely cause:** the xterm linkifier did not register the synthetic `mousemove` target. This looks like a hover hit-testing/settling race, not the prior permanent-gap regression: the terminal was already its full height. The test calculates coordinates from `screen.getBoundingClientRect()` and dispatches an untrusted `MouseEvent`, which is fragile across renderer fit/reflow timing.
- **Affected area:** `tests/e2e/issue-12656-terminal-link-tooltip.spec.ts`; investigate `src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts` and terminal tooltip geometry CSS.
- **Ownership clue:** introduced/last materially changed by Jinwoo Hong (`Jinwoo-H`) in [#13075](https://github.com/stablyai/orca/pull/13075), commit `8c3e9535c7`.
- **Suggested fix:** reproduce with a real Playwright pointer move and wait for post-fit dimensions before testing; if it still fails, correct xterm coordinate/hit-testing after reflow. Keep an assertion that hover activates and that blur restores full terminal geometry.
## `e2e 4-of-10`: paired terminal link to Orca Browser
- **Test:** `tests/e2e/paired-remote-terminal-browser-link.spec.ts:126`, `opens a paired-runtime terminal link on its owning host`.
- **Evidence:** after choosing **Orca Browser**, the test waited 60 seconds for a single host-owned browser tab. It found no matching client workspace/page/remote handle/host tab identity (`Received: null`) at line 200.
- **Likely cause:** a paired-runtime browser-tab registration race. The terminal link action is routed to the host, but the paired client has not yet materialized the returned page identity/handle. The failure is not an external-server failure: the app completed the action but never reached the expected paired browser-tab state.
- **Affected area:** `src/renderer/src/lib/workspace-browser-tab-open.ts`, `src/renderer/src/runtime/web-runtime-session.ts`, and `tests/e2e/paired-remote-terminal-browser-link.spec.ts`.
- **Ownership clue:** regression path was introduced by Jinwoo Hong (`Jinwoo-H`) in [#14117](https://github.com/stablyai/orca/pull/14117), commit `e8350f1990`.
- **Known follow-up:** after this run, [#14363](https://github.com/stablyai/orca/pull/14363) (`232e398c35`, refined by `2f85dda183`, author `OrcaWin`/`m4air`) added a registration-race guard in this exact area. Validate this shard on a commit containing that PR before making a second fix.
## `e2e 8-of-10`: background terminal-output scheduler
- **Test:** `tests/e2e/terminal-output-scheduler.spec.ts:179`, `background tab output bursts use the shared drain while the active tab renders`.
- **Evidence:** line 282 expected the largest recorded drain to be at most 2 writes, but received 4. The same shard passed the foreground-marker and background-retention checks, then reported `53 passed`, `2 skipped`, `1 failed`.
- **Likely cause:** the test asserts the normal `MAX_WRITES_PER_DRAIN = 2` for every drain, while the scheduler intentionally uses `HIGH_PRIORITY_MAX_WRITES_PER_DRAIN = 8` whenever a high-priority/large backlog is present. A 4-write drain is therefore consistent with the implementation's high-priority path; determine whether the burst is incorrectly promoted or the test needs to observe and exempt high-priority drains.
- **Affected area:** `src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts:1039-1041`, its unit tests, and `tests/e2e/terminal-output-scheduler.spec.ts`.
- **Ownership clue:** scheduler scenario originated with Brennan Benson (`brennanb2025`) in [#1699](https://github.com/stablyai/orca/pull/1699), commit `daaea6acaa`; the high-priority branch was added by Neil in `e896dd3eb7`.
- **Suggested fix:** expose whether each debug drain used high priority. Assert `<= 2` only for normal drains; separately assert the high-priority cap and preserve the existing foreground-progress check. If this burst should never be high priority, trace `entry.highPriority`/queued character transitions and fix the promotion instead.
## Notes
- Repeated `spawn codex ENOENT`, DBus, and legacy-worker-liveness messages occurred in both failed and passing tests; they are environmental/background noise, not the terminal assertions that failed.
- Artifacts are available for each failed shard: [3-of-10](https://github.com/stablyai/orca/actions/runs/31749134781/artifacts/9200825983), [4-of-10](https://github.com/stablyai/orca/actions/runs/31749134781/artifacts/9200955451), and [8-of-10](https://github.com/stablyai/orca/actions/runs/31749134781/artifacts/9200828715).
@@ -1315,6 +1315,8 @@ describe('pane terminal output scheduler', () => {
peakQueuedChars: number
peakQueuedCharsByTerminal: number
droppedBacklogCount: number
drainWrites: number[]
drainHighPriority: boolean[]
}
}
}
@@ -1340,7 +1342,9 @@ describe('pane terminal output scheduler', () => {
peakQueuedTerminalCount: 2,
peakQueuedChars: 30,
peakQueuedCharsByTerminal: 20,
droppedBacklogCount: 0
droppedBacklogCount: 0,
drainWrites: [2],
drainHighPriority: [false]
})
})
@@ -1368,6 +1372,13 @@ describe('pane terminal output scheduler', () => {
vi.useFakeTimers()
const { writeTerminalOutput } = await loadScheduler()
const terminal = createTerminal()
const debug = (
window as unknown as {
__terminalOutputSchedulerDebug?: {
snapshot: () => { drainWrites: number[]; drainHighPriority: boolean[] }
}
}
).__terminalOutputSchedulerDebug
const chunk = 'x'.repeat(16 * 1024)
for (let i = 0; i < 64; i++) {
@@ -1381,9 +1392,17 @@ describe('pane terminal output scheduler', () => {
// parser's pace instead of a fixed 2-write drip.
vi.advanceTimersByTime(0)
expect(terminal.write).toHaveBeenCalledTimes(8)
expect(debug?.snapshot()).toMatchObject({
drainWrites: [8],
drainHighPriority: [true]
})
vi.advanceTimersByTime(4)
expect(terminal.write).toHaveBeenCalledTimes(16)
expect(debug?.snapshot()).toMatchObject({
drainWrites: [8, 8],
drainHighPriority: [true, true]
})
})
it('yields high-priority backlog drains when writes spend the frame budget', async () => {
@@ -188,6 +188,7 @@ type TerminalOutputSchedulerDebugSnapshot = {
peakQueuedCharsByTerminal: number
droppedBacklogCount: number
drainWrites: number[]
drainHighPriority: boolean[]
}
type TerminalOutputSchedulerDebugApi = {
@@ -209,7 +210,8 @@ const debugState: TerminalOutputSchedulerDebugSnapshot = {
peakQueuedChars: 0,
peakQueuedCharsByTerminal: 0,
droppedBacklogCount: 0,
drainWrites: []
drainWrites: [],
drainHighPriority: []
}
function resetDebugState(): void {
@@ -227,6 +229,7 @@ function resetDebugState(): void {
debugState.peakQueuedCharsByTerminal = 0
debugState.droppedBacklogCount = 0
debugState.drainWrites = []
debugState.drainHighPriority = []
}
function readQueueDebugSnapshot(): {
@@ -279,7 +282,8 @@ function exposeDebugApi(): void {
recordQueueDebugPressure()
return {
...debugState,
drainWrites: [...debugState.drainWrites]
drainWrites: [...debugState.drainWrites],
drainHighPriority: [...debugState.drainHighPriority]
}
}
}
@@ -1036,9 +1040,8 @@ function drainQueuedOutput(): void {
drainTimerDelayMs = null
let writes = 0
const startedAt = getDrainNow()
const maxWrites = hasHighPriorityBacklog()
? HIGH_PRIORITY_MAX_WRITES_PER_DRAIN
: MAX_WRITES_PER_DRAIN
const highPriority = hasHighPriorityBacklog()
const maxWrites = highPriority ? HIGH_PRIORITY_MAX_WRITES_PER_DRAIN : MAX_WRITES_PER_DRAIN
while (queuedByTerminal.size > 0 && writes < maxWrites) {
const entry = takeNextDrainableEntry()
@@ -1071,6 +1074,7 @@ function drainQueuedOutput(): void {
if (debugEnabled && writes > 0) {
debugState.drainWrites.push(writes)
debugState.drainHighPriority.push(highPriority)
}
recordQueueDebugPressure()
if (queuedByTerminal.size > 0 && hasDrainableBacklog()) {
@@ -20,7 +20,6 @@ type LinkProbe = {
type TooltipState = {
display: string
text: string
currentLinkText: string | null
cursor: string
paneBottom: number
terminalBottom: number
@@ -62,23 +61,24 @@ async function locateUrl(page: Page, url: string): Promise<LinkProbe | null> {
}
async function moveToLink(page: Page, probe: LinkProbe): Promise<void> {
await page.evaluate(({ col, row, tabId }) => {
const point = await page.evaluate(({ col, row, tabId }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
if (!pane || !screen) {
throw new Error('xterm-screen element unavailable')
}
const cell = pane.terminal.dimensions?.css.cell
if (!cell?.width || !cell.height) {
throw new Error('terminal cell dimensions unavailable')
}
const rect = screen.getBoundingClientRect()
screen.dispatchEvent(
new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: rect.left + (col + 0.5) * (rect.width / pane.terminal.cols),
clientY: rect.top + (row + 0.5) * (rect.height / pane.terminal.rows)
})
)
return {
x: rect.left + (col + 0.5) * cell.width,
y: rect.top + (row + 0.5) * cell.height
}
}, probe)
await page.mouse.move(point.x, point.y)
}
async function readTooltipState(page: Page, tabId: string): Promise<TooltipState> {
@@ -90,11 +90,6 @@ async function readTooltipState(page: Page, tabId: string): Promise<TooltipState
throw new Error('terminal pane unavailable')
}
const linkifier = (
pane.terminal as unknown as {
_core?: { linkifier?: { currentLink?: { link?: { text?: string } } } }
}
)._core?.linkifier
const paneRect = pane.container.getBoundingClientRect()
const terminalRect = pane.terminal.element?.parentElement?.getBoundingClientRect()
const tooltipRect = pane.linkTooltip.getBoundingClientRect()
@@ -102,7 +97,6 @@ async function readTooltipState(page: Page, tabId: string): Promise<TooltipState
return {
display: pane.linkTooltip.style.display,
text: pane.linkTooltip.textContent ?? '',
currentLinkText: linkifier?.currentLink?.link?.text ?? null,
cursor: getComputedStyle(screen).cursor,
paneBottom: paneRect.bottom,
terminalBottom: terminalRect?.bottom ?? 0,
@@ -156,7 +150,7 @@ test.describe('Issue #12656 terminal link tooltip', () => {
await moveToLink(orcaPage, probe)
return readTooltipState(orcaPage, probe.tabId)
})
.toMatchObject({ display: '', currentLinkText: url })
.toMatchObject({ display: '', text: expect.stringContaining(url) })
const hovered = await readTooltipState(orcaPage, probe.tabId)
expect(hovered.text).toContain(url)
@@ -169,7 +163,7 @@ test.describe('Issue #12656 terminal link tooltip', () => {
await orcaPage.evaluate(() => window.dispatchEvent(new Event('blur')))
await expect
.poll(() => readTooltipState(orcaPage, probe.tabId))
.toMatchObject({ display: 'none', currentLinkText: null, cursor: 'text' })
.toMatchObject({ display: 'none', cursor: 'text' })
const cleared = await readTooltipState(orcaPage, probe.tabId)
expect(Math.abs(cleared.paneBottom - cleared.terminalBottom)).toBeLessThanOrEqual(1)
await captureProof(orcaPage, testInfo, 'issue-12656-fixed-after-blur.png')
+4 -2
View File
@@ -26,6 +26,7 @@ type SchedulerDebugSnapshot = {
flushWriteCount: number
scheduledDrainCount: number
drainWrites: number[]
drainHighPriority: boolean[]
}
type SchedulerDebugWindow = Window & {
@@ -278,8 +279,9 @@ test.describe('Terminal output scheduler', () => {
const debug = await getSchedulerDebug(orcaPage)
expect(debug.foregroundWriteCount).toBeGreaterThan(0)
if (debug.drainWrites.length > 0) {
expect(Math.max(...debug.drainWrites)).toBeLessThanOrEqual(2)
expect(debug.drainHighPriority).toHaveLength(debug.drainWrites.length)
for (const [index, writes] of debug.drainWrites.entries()) {
expect(writes).toBeLessThanOrEqual(debug.drainHighPriority[index] ? 8 : 2)
}
const firstBackground = backgroundCommands[0]