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 <sim@local>
This commit is contained in:
Brennan Benson
2026-09-11 18:37:16 -07:00
committed by GitHub
co-authored by Merge Sim
parent 113e58f34e
commit 556a7772ed
6 changed files with 43 additions and 9 deletions
@@ -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') {
@@ -97,6 +97,10 @@ async function releaseHeldLinearLookup(page: Page): Promise<void> {
async function pasteLinearUrl(page: Page, input: ReturnType<Page['locator']>): Promise<void> {
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())
}
+18 -4
View File
@@ -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'
@@ -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 ({
@@ -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)
+12 -3
View File
@@ -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({