test: retire low-value draft and CDP lifecycle examples

This commit is contained in:
OrcaWin
2026-11-12 15:57:02 -08:00
parent b546bd0a23
commit 1d9fea54bf
6 changed files with 0 additions and 480 deletions
@@ -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,
@@ -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.
})
@@ -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)
})
})