mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
test(mobile): share complete RPC client double
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createElement, useEffect } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { HostProtocolGate, useHostProtocolGates } from './HostProtocolGate'
|
||||
|
||||
@@ -36,7 +37,9 @@ vi.mock('../transport/client-context', () => ({
|
||||
}))
|
||||
|
||||
function clientWithStatus(result: Record<string, unknown>): RpcClient {
|
||||
return { sendRequest: vi.fn().mockResolvedValue({ ok: true, result }) } as unknown as RpcClient
|
||||
return createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockResolvedValue({ ok: true, result })
|
||||
})
|
||||
}
|
||||
|
||||
function GateConsumer() {
|
||||
@@ -161,9 +164,9 @@ describe('HostProtocolGate', () => {
|
||||
})
|
||||
|
||||
it('does not mount host routes before a connected host passes the compatibility probe', async () => {
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockReturnValue(new Promise(() => {}))
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
hostClient.current = { client, state: 'connected' }
|
||||
renderer = await renderGate()
|
||||
const output = renderedText(renderer)
|
||||
@@ -179,9 +182,9 @@ describe('HostProtocolGate', () => {
|
||||
expect(renderedText(renderer)).toContain('HostContent')
|
||||
expect(probeMounts.count).toBe(1)
|
||||
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockReturnValue(new Promise(() => {}))
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
await act(async () => {
|
||||
hostClient.current = { client, state: 'connected' }
|
||||
renderer?.update(gateElement())
|
||||
@@ -220,7 +223,7 @@ describe('HostProtocolGate', () => {
|
||||
})
|
||||
|
||||
it('keeps an already-validated host route mounted while reconnect status is pending', async () => {
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
sendRequest: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
@@ -228,7 +231,7 @@ describe('HostProtocolGate', () => {
|
||||
result: { protocolVersion: 5, minCompatibleMobileVersion: 0 }
|
||||
})
|
||||
.mockReturnValueOnce(new Promise(() => {}))
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
hostClient.current = { client, state: 'connected' }
|
||||
renderer = await renderGate()
|
||||
|
||||
@@ -251,9 +254,9 @@ describe('HostProtocolGate', () => {
|
||||
|
||||
it('fails open when a connected host cannot answer the status probe', async () => {
|
||||
hostClient.current = {
|
||||
client: {
|
||||
client: createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockResolvedValue({ ok: false, error: { message: 'unavailable' } })
|
||||
} as unknown as RpcClient,
|
||||
}),
|
||||
state: 'connected'
|
||||
}
|
||||
renderer = await renderGate()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
|
||||
const asyncStorage = vi.hoisted(() => ({
|
||||
getItem: vi.fn().mockResolvedValue(null),
|
||||
@@ -49,6 +50,10 @@ import { NewWorktreeModal } from './NewWorktreeModal'
|
||||
|
||||
const LOCAL_HOST_LABEL = getLocalExecutionHostLabel('darwin')
|
||||
|
||||
function rpcSuccess(result: unknown): RpcResponse {
|
||||
return { id: '1', ok: true, result, _meta: { runtimeId: 'runtime-1' } }
|
||||
}
|
||||
|
||||
const repos = [
|
||||
{
|
||||
id: 'repo-1',
|
||||
@@ -100,11 +105,11 @@ describe('NewWorktreeModal project targets', () => {
|
||||
return Promise.reject(new Error('connection closed'))
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
|
||||
return Promise.resolve(rpcSuccess({ hostPlatform: 'darwin' }))
|
||||
}
|
||||
return new Promise(() => {})
|
||||
})
|
||||
const client = { sendRequest } as unknown as RpcClient
|
||||
const client = createRpcClientTestDouble({ sendRequest })
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
@@ -118,9 +123,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
)
|
||||
await Promise.resolve()
|
||||
})
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
await flushUpdates()
|
||||
|
||||
expect(sendRequest).toHaveBeenCalledWith('repo.list')
|
||||
expect(pickerItems(renderer, 'Project')).toEqual([
|
||||
@@ -143,17 +146,17 @@ describe('NewWorktreeModal project targets', () => {
|
||||
upstream: { owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
]
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockImplementation((method: string) => {
|
||||
if (method === 'repo.list') {
|
||||
return Promise.resolve({ ok: true, result: { repos: listedRepos } })
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
|
||||
return Promise.resolve(rpcSuccess({ hostPlatform: 'darwin' }))
|
||||
}
|
||||
return new Promise(() => {})
|
||||
})
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
@@ -167,6 +170,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
)
|
||||
await Promise.resolve()
|
||||
})
|
||||
await flushUpdates()
|
||||
|
||||
expect(pickerItems(renderer, 'Project')).toEqual([
|
||||
expect.objectContaining({ label: 'orca', detail: 'stablyai/orca' })
|
||||
@@ -223,14 +227,14 @@ describe('NewWorktreeModal project targets', () => {
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform: 'linux' } })
|
||||
return Promise.resolve(rpcSuccess({ hostPlatform: 'linux' }))
|
||||
}
|
||||
if (method === 'repo.hooks') {
|
||||
return Promise.resolve({ ok: true, result: { hooks: null, source: null } })
|
||||
}
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
})
|
||||
const client = { sendRequest } as unknown as RpcClient
|
||||
const client = createRpcClientTestDouble({ sendRequest })
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
@@ -279,14 +283,14 @@ describe('NewWorktreeModal project targets', () => {
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
|
||||
return Promise.resolve(rpcSuccess({ hostPlatform: 'darwin' }))
|
||||
}
|
||||
if (method === 'repo.hooks') {
|
||||
return Promise.resolve({ ok: true, result: { hooks: null, source: null } })
|
||||
}
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
})
|
||||
const client = { sendRequest } as unknown as RpcClient
|
||||
const client = createRpcClientTestDouble({ sendRequest })
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
@@ -312,25 +316,25 @@ describe('NewWorktreeModal project targets', () => {
|
||||
resolveOldList = resolve
|
||||
})
|
||||
const freshRepo = { ...repos[0]!, id: 'repo-fresh', displayName: 'fresh' }
|
||||
const oldClient = {
|
||||
const oldClient = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockImplementation((method: string) => {
|
||||
if (method === 'repo.list') {
|
||||
return oldList
|
||||
}
|
||||
return new Promise(() => {})
|
||||
})
|
||||
} as unknown as RpcClient
|
||||
const freshClient = {
|
||||
})
|
||||
const freshClient = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockImplementation((method: string) => {
|
||||
if (method === 'repo.list') {
|
||||
return Promise.resolve({ ok: true, result: { repos: [freshRepo] } })
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
|
||||
return Promise.resolve(rpcSuccess({ hostPlatform: 'darwin' }))
|
||||
}
|
||||
return new Promise(() => {})
|
||||
})
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
const modalProps = {
|
||||
visible: true,
|
||||
hostId: 'host-1',
|
||||
@@ -357,7 +361,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
|
||||
it('remounts before the reopened session renders, so no stale instance sees visible', async () => {
|
||||
const sendRequest = vi.fn().mockImplementation(() => new Promise(() => {}))
|
||||
const client = { sendRequest } as unknown as RpcClient
|
||||
const client = createRpcClientTestDouble({ sendRequest })
|
||||
const modalProps = {
|
||||
client,
|
||||
hostId: 'host-1',
|
||||
@@ -378,9 +382,9 @@ describe('NewWorktreeModal project targets', () => {
|
||||
})
|
||||
|
||||
it('starts with fresh form state after closing and reopening', async () => {
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
sendRequest: vi.fn().mockImplementation(() => new Promise(() => {}))
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
const modalProps = {
|
||||
client,
|
||||
hostId: 'host-1',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
|
||||
const probe = vi.hoisted(() => ({
|
||||
@@ -58,7 +59,7 @@ describe('useCodexResetCreditCapability', () => {
|
||||
return cancel
|
||||
}
|
||||
)
|
||||
const client = { sendRequest: vi.fn() } as unknown as RpcClient
|
||||
const client = createRpcClientTestDouble({ sendRequest: vi.fn() })
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
|
||||
function Harness() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { HostCatalogEntry } from '../transport/types'
|
||||
import {
|
||||
reconcileMobileHomeHostStates,
|
||||
@@ -22,7 +22,7 @@ function catalogHost(
|
||||
}
|
||||
|
||||
function liveClient(hostId: string, state: MobileHomeClientEntry['state']): MobileHomeClientEntry {
|
||||
return { hostId, state, client: {} as RpcClient }
|
||||
return { hostId, state, client: createRpcClientTestDouble() }
|
||||
}
|
||||
|
||||
describe('mobile home connection state', () => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
subscribeToDesktopNotifications
|
||||
} from './mobile-notifications'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { loadPushNotificationsEnabled } from '../storage/preferences'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import { resetHostNotificationSessionsForTests } from './notification-reconnect-catchup'
|
||||
|
||||
vi.mock('expo-notifications', () => ({
|
||||
@@ -93,11 +93,11 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
|
||||
it('drops the local stream when disposed before the desktop returns ready', () => {
|
||||
const unsubscribeStream = vi.fn()
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn(() => unsubscribeStream),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
const unsubscribe = subscribeToDesktopNotifications(client, 'host-1')
|
||||
unsubscribe()
|
||||
@@ -117,14 +117,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
.mockResolvedValueOnce('scheduled-2')
|
||||
vi.mocked(Notifications.dismissNotificationAsync).mockResolvedValue(undefined)
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-1')
|
||||
onEvent?.({
|
||||
@@ -173,14 +173,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
} as never)
|
||||
vi.mocked(Notifications.scheduleNotificationAsync).mockResolvedValue('scheduled-1')
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-concurrent')
|
||||
onEvent?.({
|
||||
@@ -217,14 +217,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
)
|
||||
vi.mocked(Notifications.dismissNotificationAsync).mockResolvedValue(undefined)
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-dismiss-race')
|
||||
onEvent?.({
|
||||
@@ -257,14 +257,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
.mockResolvedValueOnce('scheduled-2')
|
||||
vi.mocked(Notifications.dismissNotificationAsync).mockResolvedValue(undefined)
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-dismiss-failed-replacement')
|
||||
onEvent?.({
|
||||
@@ -304,14 +304,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
it('treats unknown dismiss events as no-ops', async () => {
|
||||
vi.mocked(Notifications.dismissNotificationAsync).mockResolvedValue(undefined)
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-unknown')
|
||||
onEvent?.({ type: 'dismiss', notificationId: 'agent:missing' })
|
||||
@@ -335,14 +335,14 @@ describe('subscribeToDesktopNotifications', () => {
|
||||
.mockResolvedValueOnce('scheduled-new')
|
||||
vi.mocked(Notifications.dismissNotificationAsync).mockResolvedValue(undefined)
|
||||
let onEvent: ((data: unknown) => void) | null = null
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method, _params, callback: (data: unknown) => void) => {
|
||||
onEvent = callback
|
||||
return vi.fn()
|
||||
}),
|
||||
getState: vi.fn(() => 'connected'),
|
||||
sendRequest: vi.fn()
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
|
||||
subscribeToDesktopNotifications(client, 'host-1')
|
||||
onEvent?.({ type: 'notification', title: 't', body: 'b', notificationId: 'agent:old' })
|
||||
@@ -386,7 +386,7 @@ describe('subscribeToDesktopNotifications — reconnect catch-up', () => {
|
||||
function makeClient() {
|
||||
let onData: ((data: unknown) => void) | null = null
|
||||
const sentRequests: { method: string; params: unknown }[] = []
|
||||
const client = {
|
||||
const client = createRpcClientTestDouble({
|
||||
subscribe: vi.fn((_method: string, _params: unknown, cb: (data: unknown) => void) => {
|
||||
onData = cb
|
||||
return vi.fn()
|
||||
@@ -399,12 +399,12 @@ describe('subscribeToDesktopNotifications — reconnect catch-up', () => {
|
||||
result: method === 'notifications.getMissedSince' ? { notifications: [] } : undefined
|
||||
}) as never
|
||||
)
|
||||
}
|
||||
})
|
||||
// Why: onData is captured live via a getter (not destructured) because the
|
||||
// subscribe mock assigns it asynchronously as a side effect of
|
||||
// subscribeToDesktopNotifications calling client.subscribe.
|
||||
return {
|
||||
client: client as unknown as RpcClient,
|
||||
client,
|
||||
get onData() {
|
||||
return onData
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
import { readNewWorktreeRuntimeCapabilities } from './worktree-create-capability'
|
||||
@@ -18,7 +19,7 @@ type StatusOutcome =
|
||||
|
||||
function statusClient(outcomes: StatusOutcome[]): RpcClient {
|
||||
let call = 0
|
||||
return {
|
||||
return createRpcClientTestDouble({
|
||||
sendRequest: async () => {
|
||||
const outcome = outcomes[Math.min(call, outcomes.length - 1)]!
|
||||
call += 1
|
||||
@@ -38,7 +39,7 @@ function statusClient(outcomes: StatusOutcome[]): RpcClient {
|
||||
_meta: { runtimeId: 'r' }
|
||||
}
|
||||
}
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
}
|
||||
|
||||
describe('readNewWorktreeRuntimeCapabilities', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createRpcClientTestDouble } from '../test/rpc-client-test-double'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
|
||||
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
@@ -70,7 +71,7 @@ function scriptedClient(
|
||||
lastInboundAt?: number | (() => number)
|
||||
): RpcClient {
|
||||
let call = 0
|
||||
return {
|
||||
return createRpcClientTestDouble({
|
||||
getState: () => connection?.getState() ?? 'connected',
|
||||
getLastInboundAt: () =>
|
||||
(typeof lastInboundAt === 'function' ? lastInboundAt() : lastInboundAt) ?? null,
|
||||
@@ -107,7 +108,7 @@ function scriptedClient(
|
||||
_meta: { runtimeId: 'r' }
|
||||
}
|
||||
}
|
||||
} as unknown as RpcClient
|
||||
})
|
||||
}
|
||||
|
||||
describe('createWorktreeWithNameRetry', () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
|
||||
export function createRpcClientTestDouble(): RpcClient
|
||||
export function createRpcClientTestDouble<T extends Partial<RpcClient>>(overrides: T): RpcClient & T
|
||||
export function createRpcClientTestDouble(overrides: Partial<RpcClient> = {}): RpcClient {
|
||||
const client: RpcClient = {
|
||||
sendRequest: () => Promise.reject(new Error('Unconfigured RPC test request')),
|
||||
subscribe: () => () => {},
|
||||
updateTerminalSubscriptionViewport: () => {},
|
||||
getState: () => 'connected',
|
||||
getReconnectAttempt: () => 0,
|
||||
getLastConnectedAt: () => null,
|
||||
getLastInboundAt: () => null,
|
||||
onStateChange: () => () => {},
|
||||
notifyForeground: () => {},
|
||||
close: () => {}
|
||||
}
|
||||
return Object.assign(client, overrides)
|
||||
}
|
||||
Reference in New Issue
Block a user