mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
test: retire low-value draft and CDP lifecycle examples
This commit is contained in:
@@ -902,9 +902,7 @@
|
||||
"src/main/browser/cdp/commands/cdp-bridge-integration.test.ts": 28361,
|
||||
"src/main/browser/cdp/commands/cdp-print-to-pdf.test.ts": 68,
|
||||
"src/main/browser/cdp/commands/cdp-screenshot.test.ts": 415,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy-first-paint.test.ts": 366,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy-host-admission.test.ts": 576,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy-input-focus.test.ts": 360,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy-origin.test.ts": 262,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy-startup-lifetime.test.ts": 326,
|
||||
"src/main/browser/cdp/transport/cdp-ws-proxy.test.ts": 2756,
|
||||
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { NativeChatMessage } from '../../../../../src/shared/native-chat/native-chat-types'
|
||||
|
||||
import { useMobileNativeChatDrafts } from './use-mobile-native-chat-drafts'
|
||||
import {
|
||||
closeMobileNativeChatTabDraft,
|
||||
useMobileNativeChatDraft
|
||||
} from './mobile-native-chat-draft-store'
|
||||
|
||||
import { loadMobileNativeChatDrafts } from './mobile-native-chat-draft-persistence'
|
||||
|
||||
vi.mock('@react-native-async-storage/async-storage', async () => {
|
||||
const { createInMemoryAsyncStorage } =
|
||||
await import('../../../test-support/in-memory-async-storage')
|
||||
return { default: createInMemoryAsyncStorage() }
|
||||
})
|
||||
|
||||
type DraftState = ReturnType<typeof useMobileNativeChatDrafts> & { composerText: string }
|
||||
type TestRenderer = {
|
||||
unmount(): void
|
||||
update(element: ReturnType<typeof createElement>): void
|
||||
}
|
||||
|
||||
function assistantTurn(id: string, text: string, timestamp: number): NativeChatMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text }],
|
||||
timestamp,
|
||||
source: 'transcript'
|
||||
}
|
||||
}
|
||||
|
||||
function imageTurn(id: string, path: string, timestamp: number): NativeChatMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: `[Image: source: ${path}]` }],
|
||||
timestamp,
|
||||
source: 'transcript'
|
||||
}
|
||||
}
|
||||
|
||||
describe('useMobileNativeChatDrafts glued pending sends', () => {
|
||||
let renderer: TestRenderer | null = null
|
||||
let state: DraftState | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
act(() => renderer?.unmount())
|
||||
await loadMobileNativeChatDrafts('host', 'worktree')
|
||||
closeMobileNativeChatTabDraft('host\0worktree\0tab')
|
||||
await loadMobileNativeChatDrafts('host', 'worktree')
|
||||
renderer = null
|
||||
state = null
|
||||
})
|
||||
|
||||
function Harness({
|
||||
messages,
|
||||
transcriptLoading = false,
|
||||
transcriptSettled = !transcriptLoading
|
||||
}: {
|
||||
messages: NativeChatMessage[]
|
||||
transcriptLoading?: boolean
|
||||
transcriptSettled?: boolean
|
||||
}): null {
|
||||
const drafts = useMobileNativeChatDrafts({
|
||||
hostId: 'host',
|
||||
worktreeId: 'worktree',
|
||||
tabId: 'tab',
|
||||
sessionId: 'session',
|
||||
messages,
|
||||
transcriptLoading,
|
||||
transcriptSettled
|
||||
})
|
||||
state = { ...drafts, composerText: useMobileNativeChatDraft(drafts.draftKey) }
|
||||
return null
|
||||
}
|
||||
|
||||
async function mount(
|
||||
messages: NativeChatMessage[],
|
||||
transcriptLoading = false,
|
||||
transcriptSettled = !transcriptLoading
|
||||
): Promise<void> {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness, { messages, transcriptLoading, transcriptSettled }))
|
||||
})
|
||||
}
|
||||
|
||||
async function update(
|
||||
messages: NativeChatMessage[],
|
||||
transcriptLoading = false,
|
||||
transcriptSettled = !transcriptLoading
|
||||
): Promise<void> {
|
||||
await act(async () =>
|
||||
renderer?.update(createElement(Harness, { messages, transcriptLoading, transcriptSettled }))
|
||||
)
|
||||
}
|
||||
|
||||
// Between the older history rows and the rows that land after a send, on the host's clock.
|
||||
const SENT_AT = 3000
|
||||
|
||||
// Hydrating rows give no boundary, so these sends match only rows newer than their send time.
|
||||
describe('sends issued while the transcript is still hydrating', () => {
|
||||
// An image retires only by preview binding; excluding its own echo would strand both the photo and later glued sends.
|
||||
it('retires a photo whose image echo arrives with the read', async () => {
|
||||
await mount([], true)
|
||||
act(() => {
|
||||
const origin = state?.captureSendOrigin('', SENT_AT)
|
||||
if (origin) {
|
||||
state?.acceptSend(origin, '', ['file:///new.png'])
|
||||
}
|
||||
})
|
||||
|
||||
const withEcho = [assistantTurn('m1', 'ready', 1000), imageTurn('m2', '/tmp/new.png', 5000)]
|
||||
await update(withEcho)
|
||||
expect(state?.pending).toEqual([])
|
||||
expect(state?.imagePreviewsByMessageId).toEqual({ m2: ['file:///new.png'] })
|
||||
})
|
||||
})
|
||||
|
||||
// Failed-read emptiness is unknown history, not an empty conversation; old rows must not claim sends against it.
|
||||
})
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
import { closeMobileNativeChatEditInputs } from './mobile-native-chat-draft-inputs'
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import '../../../../../src/shared/agent-launch/startup/agent-tui-input-clear'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../../../transport/rpc/rpc-client-contract'
|
||||
import type { RpcResponse, RpcSuccess } from '../../../transport/rpc/rpc-wire-types'
|
||||
import { resetMobileNativeChatStaleInputForTests } from './mobile-native-chat-stale-input'
|
||||
import { resetMobileNativeChatTerminalWritesForTests } from './mobile-native-chat-terminal-write-lock'
|
||||
import { useMobileNativeChatImageAttachments } from './use-mobile-native-chat-image-attachments'
|
||||
|
||||
// Stub the media seam so direct Expo imports would expose unsupported Flow syntax and fail the transform.
|
||||
const pick = vi.hoisted(() => vi.fn())
|
||||
const ids = vi.hoisted(() => ({ next: 0 }))
|
||||
vi.mock('../../../../../src/shared/non-secure-context-uuid', () => ({
|
||||
createNonSecureContextUuid: () => `img-${++ids.next}`
|
||||
}))
|
||||
vi.mock('../../../platform/media-picker', () => ({ useMediaPicker: () => ({ pickImages: pick }) }))
|
||||
// The canvas fit needs a WebView; this test is about the attaching counter.
|
||||
vi.mock('../../../files/attachments/mobile-agent-image-fit', () => ({
|
||||
fitMobileAgentImage: async (image: unknown) => image
|
||||
}))
|
||||
|
||||
function ok(id: string, result: unknown): RpcSuccess {
|
||||
return { id, ok: true, result, _meta: { runtimeId: 'r' } }
|
||||
}
|
||||
function methodNotFound(id: string): RpcResponse {
|
||||
return {
|
||||
id,
|
||||
ok: false,
|
||||
error: { code: 'method_not_found', message: 'no' },
|
||||
_meta: { runtimeId: 'r' }
|
||||
}
|
||||
}
|
||||
|
||||
function makeClient(responses: (RpcResponse | Promise<RpcResponse>)[]): Pick<
|
||||
RpcClient,
|
||||
'sendRequest'
|
||||
> & {
|
||||
calls: { method: string; params: Record<string, unknown> }[]
|
||||
} {
|
||||
const calls: { method: string; params: Record<string, unknown> }[] = []
|
||||
return {
|
||||
calls,
|
||||
sendRequest: vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === 'terminal.read') {
|
||||
return ok('read', {})
|
||||
}
|
||||
calls.push({ method, params: params as Record<string, unknown> })
|
||||
const response = responses.shift()
|
||||
if (!response) {
|
||||
throw new Error(`unexpected request: ${method}`)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type HookArgs = Parameters<typeof useMobileNativeChatImageAttachments>[0]
|
||||
type Hook = ReturnType<typeof useMobileNativeChatImageAttachments>
|
||||
|
||||
const SCOPE_A = 'h\0w\0tab-a'
|
||||
|
||||
function baseArgs(overrides: Partial<HookArgs> & Pick<HookArgs, 'client'>): HookArgs {
|
||||
return {
|
||||
agent: 'claude',
|
||||
activeHandleRef: { current: 'term-1' },
|
||||
deviceTokenRef: { current: null },
|
||||
getActiveWorktreeConnectionId: async () => null,
|
||||
connState: 'connected',
|
||||
scopeKey: SCOPE_A,
|
||||
enabled: true,
|
||||
structuredNativeChat: false,
|
||||
showToast: vi.fn(),
|
||||
onSendError: vi.fn(),
|
||||
baseSend: vi.fn().mockResolvedValue('accepted'),
|
||||
readSeededLaunchDraft: () => null,
|
||||
sleep: async () => {},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('useMobileNativeChatImageAttachments', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
let hook: Hook | null = null
|
||||
let releaseConnection: ((id: string | null) => void) | null = null
|
||||
let firstAttach: Promise<void> | null = null
|
||||
|
||||
function Harness({ args }: { args: HookArgs }): null {
|
||||
hook = useMobileNativeChatImageAttachments(args)
|
||||
return null
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ids.next = 0
|
||||
pick.mockReset()
|
||||
resetMobileNativeChatStaleInputForTests()
|
||||
resetMobileNativeChatTerminalWritesForTests()
|
||||
})
|
||||
afterEach(async () => {
|
||||
act(() => renderer?.unmount())
|
||||
closeMobileNativeChatEditInputs(SCOPE_A)
|
||||
await act(async () => {
|
||||
releaseConnection?.(null)
|
||||
await firstAttach
|
||||
})
|
||||
releaseConnection = null
|
||||
firstAttach = null
|
||||
renderer = null
|
||||
hook = null
|
||||
})
|
||||
|
||||
function mount(args: HookArgs): void {
|
||||
act(() => {
|
||||
renderer = create(createElement(Harness, { args }))
|
||||
})
|
||||
}
|
||||
|
||||
it('keeps isAttaching true when a cancelled pick overlaps a genuine in-flight upload', async () => {
|
||||
// Hold a real upload after its counter increment; a concurrent cancelled pick must not decrement work it never started.
|
||||
const client = makeClient([methodNotFound('start'), ok('save', '/tmp/a.png')])
|
||||
const args = baseArgs({
|
||||
client: client as unknown as RpcClient,
|
||||
getActiveWorktreeConnectionId: () =>
|
||||
new Promise<string | null>((resolve) => {
|
||||
releaseConnection = resolve
|
||||
})
|
||||
})
|
||||
mount(args)
|
||||
|
||||
pick.mockResolvedValue([{ base64: 'AAAA', uri: 'file:///a.jpg' }])
|
||||
await act(async () => {
|
||||
firstAttach = hook!.attachImage('library')
|
||||
for (let i = 0; i < 50 && !releaseConnection; i++) {
|
||||
await Promise.resolve()
|
||||
}
|
||||
})
|
||||
expect(releaseConnection).not.toBeNull()
|
||||
expect(hook!.isAttaching).toBe(true)
|
||||
|
||||
pick.mockResolvedValue([])
|
||||
await act(async () => {
|
||||
await hook!.attachImage('library')
|
||||
})
|
||||
expect(hook!.isAttaching).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
releaseConnection!('conn-1')
|
||||
await firstAttach
|
||||
})
|
||||
expect(hook!.isAttaching).toBe(false)
|
||||
expect(hook!.attachments).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { CdpWsProxy } from './cdp-ws-proxy'
|
||||
import {
|
||||
connect,
|
||||
createMockWebContents,
|
||||
getSendCommandMethods,
|
||||
mockAsWebContents,
|
||||
sendAndReceive
|
||||
} from './cdp-ws-proxy-test-harness'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
webContents: { fromId: vi.fn() }
|
||||
}))
|
||||
|
||||
describe('CdpWsProxy first-paint gate', () => {
|
||||
it('lets an unpainted page paint before input reaches it, once per document', async () => {
|
||||
const guest = createMockWebContents()
|
||||
guest.webContents.getType.mockReturnValue('webview')
|
||||
let paintFrame = (): void => {}
|
||||
// A blank document: frames run, but no paint entry is ever recorded.
|
||||
guest.webContents.debugger.sendCommand.mockImplementation(
|
||||
async (method?: string, params?: Record<string, unknown>) => {
|
||||
if (method !== 'Runtime.evaluate') {
|
||||
return {}
|
||||
}
|
||||
if (params?.returnByValue) {
|
||||
return { result: { value: [false, 1234.5] } }
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
paintFrame = resolve
|
||||
})
|
||||
return {}
|
||||
}
|
||||
)
|
||||
const releasePaint = vi.fn()
|
||||
const holdPaint = vi.fn(() => releasePaint)
|
||||
const guestProxy = new CdpWsProxy(mockAsWebContents(guest), holdPaint)
|
||||
const client = await connect(await guestProxy.start())
|
||||
const move = (id: number): Promise<Record<string, unknown>> =>
|
||||
sendAndReceive(client, {
|
||||
id,
|
||||
method: 'Input.dispatchMouseEvent',
|
||||
params: { type: 'mouseMoved', x: 1, y: 1 }
|
||||
})
|
||||
|
||||
const first = move(33)
|
||||
await vi.waitFor(() => expect(holdPaint).toHaveBeenCalledTimes(1))
|
||||
expect(getSendCommandMethods(guest)).not.toContain('Input.dispatchMouseEvent')
|
||||
paintFrame()
|
||||
await first
|
||||
await move(34)
|
||||
|
||||
expect(getSendCommandMethods(guest).filter((m) => m.startsWith('Input.'))).toHaveLength(2)
|
||||
expect(holdPaint).toHaveBeenCalledTimes(1)
|
||||
expect(releasePaint).toHaveBeenCalledTimes(1)
|
||||
client.close()
|
||||
await guestProxy.stop()
|
||||
})
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { CdpWsProxy } from './cdp-ws-proxy'
|
||||
import {
|
||||
connect,
|
||||
createMockWebContents,
|
||||
getSendCommandMethods,
|
||||
mockAsWebContents,
|
||||
sendAndReceive
|
||||
} from './cdp-ws-proxy-test-harness'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
webContents: { fromId: vi.fn() }
|
||||
}))
|
||||
|
||||
describe('CdpWsProxy agent input focus guard', () => {
|
||||
it('refuses a press when the user’s focus cannot be guarded', async () => {
|
||||
const mock = createMockWebContents()
|
||||
mock.webContents.getType.mockReturnValue('webview')
|
||||
const proxy = new CdpWsProxy(
|
||||
mockAsWebContents(mock),
|
||||
() => () => {},
|
||||
async () => {
|
||||
throw new Error('focus guard unavailable')
|
||||
}
|
||||
)
|
||||
const client = await connect(await proxy.start())
|
||||
|
||||
const response = await sendAndReceive(client, {
|
||||
id: 81,
|
||||
method: 'Input.dispatchMouseEvent',
|
||||
params: { type: 'mousePressed', x: 1, y: 1 }
|
||||
})
|
||||
|
||||
expect(response.error).toMatchObject({ message: 'focus guard unavailable' })
|
||||
expect(getSendCommandMethods(mock)).not.toContain('Input.dispatchMouseEvent')
|
||||
client.close()
|
||||
await proxy.stop()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CdpWsProxy } from './cdp-ws-proxy'
|
||||
import { acquireElectronDebugger } from '../debugger/electron-debugger-lease'
|
||||
import { createMockWebContents, mockAsWebContents } from './cdp-ws-proxy-test-harness'
|
||||
|
||||
const { createServerMock, webSocketServerMock } = vi.hoisted(() => ({
|
||||
@@ -53,15 +52,6 @@ function pendingEnable(mock: ReturnType<typeof createMockWebContents>) {
|
||||
return { finish, fail }
|
||||
}
|
||||
|
||||
function invalidateNativeHandle(mock: ReturnType<typeof createMockWebContents>): void {
|
||||
mock.destroy()
|
||||
Object.defineProperty(mock.webContents, 'debugger', {
|
||||
get() {
|
||||
throw new Error('Object has been destroyed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('CdpWsProxy startup ownership', () => {
|
||||
let servers: ProxyServer[]
|
||||
let socketServers: ProxyWebSocketServer[]
|
||||
@@ -100,72 +90,6 @@ describe('CdpWsProxy startup ownership', () => {
|
||||
expect(mock.listeners.get('detach') ?? []).toHaveLength(0)
|
||||
}
|
||||
|
||||
it('tolerates Page.enable rejection on a live shared attachment', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const debuggerHandle = mock.webContents.debugger
|
||||
const lease = acquireElectronDebugger(mockAsWebContents(mock))
|
||||
debuggerHandle.sendCommand.mockRejectedValueOnce(new Error('Page domain is not ready'))
|
||||
const proxy = makeProxy(mock)
|
||||
|
||||
await expect(proxy.start()).resolves.toMatch(/^ws:\/\/127\.0\.0\.1:9222\//)
|
||||
await proxy.stop()
|
||||
|
||||
expectNoListeners(mock)
|
||||
expect(debuggerHandle.isAttached()).toBe(true)
|
||||
expect(debuggerHandle.detach).not.toHaveBeenCalled()
|
||||
lease.release()
|
||||
expect(debuggerHandle.attach).toHaveBeenCalledTimes(1)
|
||||
expect(debuggerHandle.detach).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'refuses server admission after native detach during Page.enable (destroyed: %s)',
|
||||
async (destroyed) => {
|
||||
const mock = createMockWebContents()
|
||||
const debuggerHandle = mock.webContents.debugger
|
||||
const enable = pendingEnable(mock)
|
||||
const proxy = makeProxy(mock)
|
||||
const starting = proxy.start()
|
||||
debuggerHandle.isAttached.mockReturnValue(false)
|
||||
if (destroyed) {
|
||||
invalidateNativeHandle(mock)
|
||||
}
|
||||
mock.emit('detach', {}, 'target closed')
|
||||
enable.fail(new Error('target closed while handling command'))
|
||||
|
||||
await expect(starting).rejects.toThrow('detached during startup')
|
||||
expect(proxy.getPort()).toBe(0)
|
||||
expect(servers).toHaveLength(0)
|
||||
expect(socketServers).toHaveLength(0)
|
||||
expectNoListeners(mock)
|
||||
}
|
||||
)
|
||||
|
||||
it('cleans a destroyed startup even when no native detach event arrives', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const enable = pendingEnable(mock)
|
||||
const proxy = makeProxy(mock)
|
||||
const starting = proxy.start()
|
||||
invalidateNativeHandle(mock)
|
||||
enable.fail(new Error('target closed while handling command'))
|
||||
|
||||
await expect(starting).rejects.toThrow('no longer available')
|
||||
expect(servers).toHaveLength(0)
|
||||
expectNoListeners(mock)
|
||||
})
|
||||
|
||||
it('refuses server admission when stopped after attachment completes', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const proxy = makeProxy(mock)
|
||||
const starting = proxy.start()
|
||||
await Promise.resolve()
|
||||
await proxy.stop()
|
||||
|
||||
await expect(starting).rejects.toThrow('stopped')
|
||||
expect(servers).toHaveLength(0)
|
||||
expectNoListeners(mock)
|
||||
})
|
||||
|
||||
it('does not detach a successor when stopped Page.enable completes late', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const debuggerHandle = mock.webContents.debugger
|
||||
@@ -187,30 +111,4 @@ describe('CdpWsProxy startup ownership', () => {
|
||||
expect(debuggerHandle.detach).toHaveBeenCalledTimes(2)
|
||||
expectNoListeners(mock)
|
||||
})
|
||||
|
||||
it('retires an established proxy without reading a destroyed native handle', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const proxy = makeProxy(mock)
|
||||
await proxy.start()
|
||||
invalidateNativeHandle(mock)
|
||||
|
||||
await expect(proxy.stop()).resolves.toBeUndefined()
|
||||
expectNoListeners(mock)
|
||||
expect(servers[0].listening).toBe(false)
|
||||
expect(socketServers[0].closed).toBe(true)
|
||||
})
|
||||
|
||||
it('closes server resources even if debugger cleanup throws', async () => {
|
||||
const mock = createMockWebContents()
|
||||
const proxy = makeProxy(mock)
|
||||
await proxy.start()
|
||||
mock.webContents.debugger.removeListener.mockImplementationOnce(() => {
|
||||
throw new Error('debugger cleanup failed')
|
||||
})
|
||||
|
||||
await expect(proxy.stop()).rejects.toThrow('debugger cleanup failed')
|
||||
expect(servers[0].listening).toBe(false)
|
||||
expect(socketServers[0].closed).toBe(true)
|
||||
expect(mock.webContents.debugger.detach).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user