mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
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:
co-authored by
Merge Sim
parent
113e58f34e
commit
556a7772ed
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user