From 556a7772ed7bdf67d5f811449d07f06dcd4285f4 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:37:16 -0700 Subject: [PATCH] fix(e2e): remove four real flake sources and one caret race (#20169) Four E2E specs failed once each across six main runs. Each traces to a timing boundary the test could not control, not to product instability: - linear-url-workspace-entry: pasted before X selection ownership landed, delivering stale text. Gate on a clipboard read-back. - native-chat-first-flush-race: a bare 1_500ms sleep is exactly UNFLUSHED_SETTLE_MS, so it straddled the boundary deciding which of two hydration paths carried the test. Observe the not-yet-flushed read instead; a notFound is never cached, so this cannot perturb hydration. - orchestration-idle-mail-delivery: asserted that a PTY -> daemon -> main round trip beats a 500ms production heuristic. Use the existing ORCA_E2E_ORCHESTRATION_POINTER_ENTER_DELAY_MS knob. - tasks-page: the probe timeout was the one figure in the file not derived from GITHUB_TASK_SEARCH_IDLE_MS. worktree.spec.ts exposed a real product race rather than a test bug: the emoji caret-restore frame stayed armed through ordinary typing, so a late frame could yank the caret back mid-input. Cancel it on the non-emoji onChange path. Also repairs a stale assertion: #20025 changed remountTerminalTabForRecovery to return a result object and updated the sibling call site but missed this one, so the comparison to `true` could never pass. It is a deterministic break, not a flake. Co-authored-by: Merge Sim --- .../smart-workspace-name-input-surface.tsx | 2 ++ tests/e2e/linear-url-workspace-entry.spec.ts | 4 ++++ .../e2e/native-chat-first-flush-race.spec.ts | 22 +++++++++++++++---- .../orchestration-idle-mail-delivery.spec.ts | 7 +++++- .../e2e/slept-workspace-remount-wake.spec.ts | 2 +- tests/e2e/tasks-page.spec.ts | 15 ++++++++++--- 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx b/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx index 277f24da30b..d43695e1c01 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx +++ b/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx @@ -192,6 +192,8 @@ export function renderSmartWorkspaceNameInput( applyEmojiReplacement(completedEmoji) return } + // A pending emoji caret frame would otherwise yank the caret back mid-typing. + cancelLocalInputFocusFrame() onValueChange(nextValue) setEmojiCursor(nextCursor) if (!disabled && mode !== 'text') { diff --git a/tests/e2e/linear-url-workspace-entry.spec.ts b/tests/e2e/linear-url-workspace-entry.spec.ts index 76e17e11ffe..d1cb2677257 100644 --- a/tests/e2e/linear-url-workspace-entry.spec.ts +++ b/tests/e2e/linear-url-workspace-entry.spec.ts @@ -97,6 +97,10 @@ async function releaseHeldLinearLookup(page: Page): Promise { async function pasteLinearUrl(page: Page, input: ReturnType): Promise { await page.evaluate((text) => window.api.ui.writeClipboardText(text), LINEAR_URL) + // X selection ownership is async; pasting before it lands delivers stale text. + await expect + .poll(() => page.evaluate(() => window.api.ui.readClipboardText()), { timeout: 5_000 }) + .toBe(LINEAR_URL) await input.focus() await page.keyboard.press(pasteChord()) } diff --git a/tests/e2e/native-chat-first-flush-race.spec.ts b/tests/e2e/native-chat-first-flush-race.spec.ts index 1362a882902..2e85e2dc132 100644 --- a/tests/e2e/native-chat-first-flush-race.spec.ts +++ b/tests/e2e/native-chat-first-flush-race.spec.ts @@ -141,10 +141,24 @@ test.describe('Native chat first-flush transcript race (#8401)', () => { path: path.join(screenshotDir, '01-loading-no-error.png') }) - // Why: a short real delay proves the first readSession attempt already - // hit the not-yet-flushed file (returning notFound) and the renderer's - // backoff retry — not a lucky first read — is what picks it up below. - await orcaPage.waitForTimeout(1_500) + // Why observe, not sleep: 1_500ms is exactly UNFLUSHED_SETTLE_MS, so a fixed + // wait straddles the boundary where the host reports the transcript pending + // and the renderer cancels its own retry. Read through the same IPC instead, + // proving the miss directly. A notFound is never cached, so this cannot + // perturb the hydration the assertions below measure. + await expect + .poll( + () => + orcaPage.evaluate( + ({ id, file }) => + window.api.nativeChat + .readSession('claude', id, 50, file) + .then((result) => Boolean(result && 'error' in result && result.notFound)), + { id: sessionId, file: transcriptPath } + ), + { timeout: 10_000, message: 'transcript resolved before the first flush' } + ) + .toBe(true) await expect(orcaPage.getByText(ERROR_TITLE)).toHaveCount(0) const userText = 'Explain the native chat first-flush race fix for #8401' diff --git a/tests/e2e/orchestration-idle-mail-delivery.spec.ts b/tests/e2e/orchestration-idle-mail-delivery.spec.ts index cffa6415208..9d90aed1875 100644 --- a/tests/e2e/orchestration-idle-mail-delivery.spec.ts +++ b/tests/e2e/orchestration-idle-mail-delivery.spec.ts @@ -653,7 +653,12 @@ test.describe('orchestration delivery to a cold-parked agent', () => { const parkingDelayMs = 500 test.use({ - orcaAppExtraEnv: { ORCA_E2E_TERMINAL_PARKING_DELAY_MS: String(parkingDelayMs) } + orcaAppExtraEnv: { + ORCA_E2E_TERMINAL_PARKING_DELAY_MS: String(parkingDelayMs), + // The working-title round trip (PTY -> daemon -> main) must beat the Enter + // timer; 500ms is a production heuristic, not a budget CI can honour. + ORCA_E2E_ORCHESTRATION_POINTER_ENTER_DELAY_MS: '5000' + } }) test('keeps one pointer and one idempotent prompt on the same parked PTY', async ({ diff --git a/tests/e2e/slept-workspace-remount-wake.spec.ts b/tests/e2e/slept-workspace-remount-wake.spec.ts index f820a7c7f69..54109a6b532 100644 --- a/tests/e2e/slept-workspace-remount-wake.spec.ts +++ b/tests/e2e/slept-workspace-remount-wake.spec.ts @@ -66,7 +66,7 @@ test('remounting a slept hidden pane does not respawn its PTY', async ({ orcaPag expect(sample.tabPtyHints[0], 'sleep must keep the session id as a wake hint').toBeTruthy() const remounted = await orcaPage.evaluate( - (tabId) => window.__store?.getState().remountTerminalTabForRecovery(tabId) ?? false, + (tabId) => window.__store?.getState().remountTerminalTabForRecovery(tabId).remounted ?? false, sleptTabId ) expect(remounted, 'remountTerminalTabForRecovery did not find the slept tab').toBe(true) diff --git a/tests/e2e/tasks-page.spec.ts b/tests/e2e/tasks-page.spec.ts index b8f57a1996f..962f86de0c5 100644 --- a/tests/e2e/tasks-page.spec.ts +++ b/tests/e2e/tasks-page.spec.ts @@ -13,6 +13,9 @@ import { GITHUB_TASK_SEARCH_IDLE_MS } from '../../src/renderer/src/components/us // on a loaded runner, so one slow keystroke committed a prefix and failed the assertion. const TASK_SEARCH_TYPING_DELAY_MS = Math.round(GITHUB_TASK_SEARCH_IDLE_MS / 6) const TASK_SEARCH_SETTLE_MS = GITHUB_TASK_SEARCH_IDLE_MS + 50 +// Why derived: the probe must outlast the idle window plus a React commit and two +// store round trips; a flat 2s left ~1.2s of slack on a single-worker runner. +const TASK_SEARCH_PROBE_TIMEOUT_MS = GITHUB_TASK_SEARCH_IDLE_MS * 6 type RenderedTaskSource = { source: string @@ -411,7 +414,9 @@ test.describe('Tasks page', () => { await input.fill('') await expect - .poll(async () => readTaskSearchRequestProbe(orcaPage), { timeout: 2_000 }) + .poll(async () => readTaskSearchRequestProbe(orcaPage), { + timeout: TASK_SEARCH_PROBE_TIMEOUT_MS + }) .toEqual({ countQueries: ['is:issue is:open'], fetchQueries: ['is:issue is:open'] }) await resetTaskSearchRequestProbe(orcaPage) @@ -422,7 +427,9 @@ test.describe('Tasks page', () => { // The contract is that no prefix of the typed query is ever queried, not that the // probe is empty at one instant: exactly one request per surface, for the final value. await expect - .poll(async () => readTaskSearchRequestProbe(orcaPage), { timeout: 2_000 }) + .poll(async () => readTaskSearchRequestProbe(orcaPage), { + timeout: TASK_SEARCH_PROBE_TIMEOUT_MS + }) .toEqual({ countQueries: ['is:issue rate'], fetchQueries: ['is:issue rate'] }) await resetTaskSearchRequestProbe(orcaPage) @@ -430,7 +437,9 @@ test.describe('Tasks page', () => { await input.press('Enter') await expect - .poll(async () => readTaskSearchRequestProbe(orcaPage), { timeout: 2_000 }) + .poll(async () => readTaskSearchRequestProbe(orcaPage), { + timeout: TASK_SEARCH_PROBE_TIMEOUT_MS + }) .toEqual({ countQueries: ['is:issue ratex'], fetchQueries: ['is:issue ratex'] }) await orcaPage.waitForTimeout(TASK_SEARCH_SETTLE_MS) expect(await readTaskSearchRequestProbe(orcaPage)).toEqual({