Files
orca/mobile/src/tasks/smart-source-paste-concurrency.test.ts
Brennan Benson 8cf7c6926e fix(mobile): create-worktree sheet dies after picking a source and loses the picked PR (#16917)
* fix(mobile): keep the create form on screen through drawer swaps and survive reconnects

The create-worktree flow could reach a state where the shared modal host was
mounted with no sheet in it: a full-screen transparent window that swallows
every tap with no way out. Frame analysis of the reported recording and a live
simulator repro both land on the same state - the form sheet laid out at the
right frame with progress=1, backdrop painting, sheet not painted.

- Keep the form sheet mounted through every drawer transition, so the host
  Modal is never on screen without a sheet, and drop the render-read pin ref.
- Re-assert a pinned sheet's enter transform when it takes the window back
  from a fill picker; nothing re-applied it before.
- Key the form session on hostId, not on the RpcClient object: useHostClient
  swaps that object on every reconnect, which silently remounted the form and
  threw away the picked source.
- Run the pasted-item lookup concurrently with the provider fan-out instead of
  after it (measured 2631ms -> 1480ms for a typed PR number).

* fix(mobile): remount the sheet view on window hand-back so a rebuilt native view repaints

On-device confirmation showed the committed hand-back re-assert never
reaches the native view: progress already sits at 1 and translateY at 0,
so withTiming produces no style delta, and the dead screen stayed
reachable (1/25 on the committed build; 1/9 with a sub-pixel value
nudge, which lands on the stale native binding when the view was rebuilt
with a new tag). Remounting the sheet's Animated.View on an epoch keyed
to the hand-back mounts a fresh native view with the style computed from
the current shared values - progress is already 1, so it paints in place
with no visible animation. 0 dead in 50 attempts on the remount build
under the same churn condition that reproduced the dead screen on base.

LANE-REPORT.md carries the full confirmation evidence and limits.

* chore: drop the stray lane report from the repo root

It is a working artifact, not source, and the root directory guard blocks
any new top-level entry.

* test(mobile): assert the sheet subtree rebuild directly, not through a test-only prop

The hand-back test proved the remount by reading an epoch-keyed nativeID that
existed only for it — production markup shaped by a test, and an assertion a
future refactor could satisfy without rebuilding anything. Count mounts of the
sheet's content instead, which is the property the fix actually depends on, and
drop the nativeID.

Also stop typing test renderers as 'ReactTestRenderer | null'. The static
analysis job installs no mobile/node_modules, so that type is unresolvable
there and the union trips no-redundant-type-constituents on every added line.

The hand-back re-assert is not dead code as the old comment implied: the drawer
swap hands back at 166ms, before the 180ms enter animation ends.

* fix(mobile): keep the create form when a render is thrown away

The session key was built from counters mutated during render. A blurred screen
suspends this subtree (react-native-screens freezes via react-freeze), so React
runs the component and then discards that render — but the counter bumps
survive it. The next committed render then produced a new key and remounted the
form, throwing away the picked source for a host switch or a close that never
committed.

Hold the open epoch in state, which React discards with the render that set it,
and put the host in the key directly instead of counting host changes.
2026-08-28 15:45:54 -07:00

60 lines
1.9 KiB
TypeScript

import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import { useSmartWorkspaceSource } from './use-smart-workspace-source'
function Probe(props: { client: RpcClient; query: string }) {
useSmartWorkspaceSource({
client: props.client,
enabled: true,
mode: 'smart',
query: props.query,
repoId: 'repo-1',
githubAvailable: true,
gitlabAvailable: false,
linearAvailable: false,
mrStateFilter: 'opened',
repos: [{ id: 'repo-1', displayName: 'orca', slug: { owner: 'stablyai', repo: 'orca' } }]
})
return null
}
// The picker makes two independent host round trips for a pasted PR number: the
// provider fan-out and the exact-item lookup. Awaiting the fan-out first stacked
// them, so the rows appeared a whole extra round trip late.
describe('smart source paste lookup concurrency', () => {
const mounted: ReactTestRenderer[] = []
beforeEach(() => vi.useFakeTimers())
afterEach(() => {
act(() => {
for (const renderer of mounted) {
renderer.unmount()
}
})
mounted.length = 0
vi.useRealTimers()
})
it('issues the pasted-number lookup while the fan-out is still in flight', async () => {
const sent: string[] = []
const sendRequest = vi.fn((method: string) => {
sent.push(method)
// Nothing ever settles: only requests issued concurrently can be observed.
return new Promise(() => {})
})
const client = { sendRequest } as unknown as RpcClient
await act(async () => {
mounted.push(create(createElement(Probe, { client, query: '16831' })))
})
await act(async () => {
await vi.advanceTimersByTimeAsync(300)
})
expect(sent).toContain('github.listWorkItems')
expect(sent).toContain('github.workItem')
})
})