mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
test: remove redundant browser and transport coverage (#33757)
* test: remove redundant browser and transport coverage * test: retire deleted suites from reliability manifest --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -497,11 +497,9 @@
|
||||
1 test-no-real-time-wait:tests/e2e/artificial-opencode-revisit-pressure-scenario.ts
|
||||
1 test-no-real-time-wait:tests/e2e/artificial-opencode-scroll-scenario.ts
|
||||
1 test-no-real-time-wait:tests/e2e/browser-guest-crash-recovery.spec.ts
|
||||
1 test-no-real-time-wait:tests/e2e/browser-settings-preview.spec.ts
|
||||
1 test-no-real-time-wait:tests/e2e/browser-tab.spec.ts
|
||||
9 test-no-real-time-wait:tests/e2e/chinese-ime-chat-input-repro.spec.ts
|
||||
2 test-no-real-time-wait:tests/e2e/cli-floating-file-open-position.spec.ts
|
||||
1 test-no-real-time-wait:tests/e2e/client-hosted-browser-tooltip-preview.spec.ts
|
||||
1 test-no-real-time-wait:tests/e2e/codex-ctrl-c-status.spec.ts
|
||||
4 test-no-real-time-wait:tests/e2e/combined-diff-scroll-restore.spec.ts
|
||||
1 test-no-real-time-wait:tests/e2e/computer-windows.e2e.ts
|
||||
@@ -1366,9 +1364,7 @@
|
||||
1 test-timeout-needs-reason:tests/e2e/agent-session-log-tail-stability.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/browser-agent-input-focus-isolation-native.spec.ts
|
||||
5 test-timeout-needs-reason:tests/e2e/browser-agent-input-focus-isolation.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/browser-settings-preview.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/cli-agent-unopened-workspace-resume.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/client-hosted-browser-tooltip-preview.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts
|
||||
1 test-timeout-needs-reason:tests/e2e/completed-worker-retirement-resume.spec.ts
|
||||
2 test-timeout-needs-reason:tests/e2e/cross-version-wire/agent-session-clear-old-client.unit.test.ts
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@ import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
mobileNativeChatComposerPlaceholder,
|
||||
useMobileNativeChatInputLease,
|
||||
useSettledMobileNativeChatInputLock
|
||||
} from './use-mobile-native-chat-input-lease'
|
||||
@@ -110,25 +109,3 @@ describe('useSettledMobileNativeChatInputLock', () => {
|
||||
expect(settled).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("the phone chat composer's placeholder", () => {
|
||||
it('says a message runs after the stop while the chat reads Stopping', () => {
|
||||
expect(mobileNativeChatComposerPlaceholder(null, 'queue')).toBe(
|
||||
'Queue a message to run after the stop'
|
||||
)
|
||||
expect(mobileNativeChatComposerPlaceholder(null, 'send')).toBe(
|
||||
'Send a message to run after the stop'
|
||||
)
|
||||
})
|
||||
|
||||
it('reads as usual otherwise', () => {
|
||||
expect(mobileNativeChatComposerPlaceholder(null, undefined)).toBe(
|
||||
'Ask anything, @ to mention files, / for commands'
|
||||
)
|
||||
})
|
||||
|
||||
it('says why the composer is locked first', () => {
|
||||
expect(mobileNativeChatComposerPlaceholder('disconnected', 'queue')).toBe('Reconnecting…')
|
||||
expect(mobileNativeChatComposerPlaceholder('waiting', 'queue')).toBe('Waiting for terminal…')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
AGENT_BROWSER_IDLE_TIMEOUT_MS,
|
||||
createAgentBrowserProcessEnvironment
|
||||
} from './agent-browser-process-environment'
|
||||
|
||||
vi.mock('node:fs', () => ({ mkdirSync: vi.fn(), chmodSync: vi.fn() }))
|
||||
|
||||
describe('agent-browser process environment', () => {
|
||||
it('bounds Unix socket paths independently of a long profile path', () => {
|
||||
const { env, ownsSocketDirectory } = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { PATH: '/bin' },
|
||||
platform: 'darwin',
|
||||
userDataPath: `/private/var/folders/${'long-profile-segment/'.repeat(12)}`
|
||||
})
|
||||
const socketDirectory = env.AGENT_BROWSER_SOCKET_DIR
|
||||
|
||||
expect(ownsSocketDirectory).toBe(true)
|
||||
expect(socketDirectory).toMatch(/^\/tmp\/orca-ab-[0-9a-f]{16}$/)
|
||||
expect(
|
||||
`${socketDirectory}/orca-tab-00000000-0000-4000-8000-000000000000.sock`.length
|
||||
).toBeLessThan(104)
|
||||
})
|
||||
|
||||
it('preserves explicit overrides and leaves Windows socket routing unchanged', () => {
|
||||
const configured = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { AGENT_BROWSER_SOCKET_DIR: '/custom/socket-dir' },
|
||||
platform: 'linux',
|
||||
userDataPath: '/profile'
|
||||
})
|
||||
expect(configured.env.AGENT_BROWSER_SOCKET_DIR).toBe('/custom/socket-dir')
|
||||
expect(configured.ownsSocketDirectory).toBe(false)
|
||||
|
||||
const windows = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { PATH: 'C:\\Windows' },
|
||||
platform: 'win32',
|
||||
userDataPath: 'C:\\Users\\Orca'
|
||||
})
|
||||
expect(windows.env.AGENT_BROWSER_SOCKET_DIR).toBeUndefined()
|
||||
expect(windows.env.PATH).toBe('C:\\Windows')
|
||||
expect(windows.ownsSocketDirectory).toBe(false)
|
||||
})
|
||||
|
||||
it.each<NodeJS.Platform>(['darwin', 'linux', 'win32'])(
|
||||
'bounds daemon idle lifetime on %s',
|
||||
(platform) => {
|
||||
const { env } = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { PATH: '/bin' },
|
||||
platform,
|
||||
userDataPath: '/profile'
|
||||
})
|
||||
expect(env.AGENT_BROWSER_IDLE_TIMEOUT_MS).toBe(String(AGENT_BROWSER_IDLE_TIMEOUT_MS))
|
||||
}
|
||||
)
|
||||
|
||||
it('never cuts a command short: idle timeout exceeds the bridge exec timeout', () => {
|
||||
expect(AGENT_BROWSER_IDLE_TIMEOUT_MS).toBeGreaterThan(90_000)
|
||||
})
|
||||
|
||||
it('honors an explicit idle timeout from the environment', () => {
|
||||
const { env } = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { AGENT_BROWSER_IDLE_TIMEOUT_MS: '5000' },
|
||||
platform: 'darwin',
|
||||
userDataPath: '/profile'
|
||||
})
|
||||
expect(env.AGENT_BROWSER_IDLE_TIMEOUT_MS).toBe('5000')
|
||||
})
|
||||
|
||||
it('still bounds the daemon when the socket directory cannot be created', async () => {
|
||||
const fs = await import('node:fs')
|
||||
vi.mocked(fs.mkdirSync).mockImplementationOnce(() => {
|
||||
throw new Error('EACCES')
|
||||
})
|
||||
const { env, ownsSocketDirectory } = createAgentBrowserProcessEnvironment({
|
||||
inheritedEnv: { PATH: '/bin' },
|
||||
platform: 'linux',
|
||||
userDataPath: '/profile'
|
||||
})
|
||||
expect(env.AGENT_BROWSER_SOCKET_DIR).toBeUndefined()
|
||||
expect(ownsSocketDirectory).toBe(false)
|
||||
expect(env.AGENT_BROWSER_IDLE_TIMEOUT_MS).toBe(String(AGENT_BROWSER_IDLE_TIMEOUT_MS))
|
||||
})
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const { lstatSync } = vi.hoisted(() => ({ lstatSync: vi.fn() }))
|
||||
vi.mock('node:fs', () => ({ lstatSync }))
|
||||
import { canSkipAgentBrowserSessionReset } from './agent-browser-session-reset'
|
||||
|
||||
const owned = {
|
||||
ownsSocketDirectory: true,
|
||||
socketDirectory: '/tmp/orca-ab-profile',
|
||||
sessionName: 'orca-tab-page'
|
||||
}
|
||||
const socketPath = join(owned.socketDirectory, 'orca-tab-page.sock')
|
||||
|
||||
beforeEach(() => {
|
||||
lstatSync.mockReset()
|
||||
})
|
||||
|
||||
it('skips an absent owned socket', () => {
|
||||
lstatSync.mockImplementation(() => {
|
||||
throw Object.assign(new Error('No socket'), { code: 'ENOENT' })
|
||||
})
|
||||
expect(canSkipAgentBrowserSessionReset(owned)).toBe(true)
|
||||
expect(lstatSync).toHaveBeenCalledWith(socketPath)
|
||||
})
|
||||
|
||||
it('requires reset when a socket or symlink exists', () => {
|
||||
lstatSync.mockReturnValue({})
|
||||
expect(canSkipAgentBrowserSessionReset(owned)).toBe(false)
|
||||
expect(lstatSync).toHaveBeenCalledWith(socketPath)
|
||||
})
|
||||
|
||||
it.each(['EACCES', 'EIO', 'ENOTDIR'])('requires reset for %s', (code) => {
|
||||
lstatSync.mockImplementation(() => {
|
||||
throw Object.assign(new Error('Socket inspection failed'), { code })
|
||||
})
|
||||
expect(canSkipAgentBrowserSessionReset(owned)).toBe(false)
|
||||
expect(lstatSync).toHaveBeenCalledWith(socketPath)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ ownsSocketDirectory: false },
|
||||
{ socketDirectory: undefined },
|
||||
{ sessionName: '../other' },
|
||||
{ sessionName: 'has space' },
|
||||
{ sessionName: '' }
|
||||
])('requires reset without an owned Unix socket address: %j', (override) => {
|
||||
expect(canSkipAgentBrowserSessionReset({ ...owned, ...override })).toBe(false)
|
||||
expect(lstatSync).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -21,8 +21,7 @@ vi.mock('electron', () => ({
|
||||
import {
|
||||
AGENT_INPUT_FOCUS_GUARD_CHANNEL,
|
||||
AGENT_INPUT_FOCUS_GUARD_READY_CHANNEL,
|
||||
guardPaneFocusForAgentInput,
|
||||
isFocusTakingCdpInput
|
||||
guardPaneFocusForAgentInput
|
||||
} from './agent-input-focus-guard'
|
||||
|
||||
function fakeRenderer(id: number, answer: boolean) {
|
||||
@@ -113,14 +112,3 @@ describe('guardPaneFocusForAgentInput', () => {
|
||||
expect(ipcListeners.get(AGENT_INPUT_FOCUS_GUARD_READY_CHANNEL)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFocusTakingCdpInput', () => {
|
||||
it('matches presses only', () => {
|
||||
expect(isFocusTakingCdpInput('Input.dispatchMouseEvent', { type: 'mousePressed' })).toBe(true)
|
||||
expect(isFocusTakingCdpInput('Input.dispatchMouseEvent', { type: 'mouseReleased' })).toBe(false)
|
||||
expect(isFocusTakingCdpInput('Input.dispatchMouseEvent', { type: 'mouseMoved' })).toBe(false)
|
||||
expect(isFocusTakingCdpInput('Input.dispatchTouchEvent', { type: 'touchStart' })).toBe(true)
|
||||
expect(isFocusTakingCdpInput('Input.synthesizeTapGesture', {})).toBe(true)
|
||||
expect(isFocusTakingCdpInput('Input.dispatchKeyEvent', { type: 'keyDown' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,18 +36,6 @@ function negotiatedTransport(
|
||||
return transport
|
||||
}
|
||||
|
||||
function boundTransport(
|
||||
availability: 'unsupported' | 'unavailable'
|
||||
): BrowserClientFileChannelTransport {
|
||||
const transport = new BrowserClientFileChannelTransport()
|
||||
transport.bind({
|
||||
fileChannelNegotiated: false,
|
||||
fileChannelAvailability: availability,
|
||||
sendFileChannelRequest: async () => ({ ok: true, result: {}, _meta: {} }) as never
|
||||
})
|
||||
return transport
|
||||
}
|
||||
|
||||
function remoteRoute(outcome: ReturnType<BrowserClientDownloadRelay['route']>) {
|
||||
expect(outcome.kind).toBe('remote')
|
||||
return (outcome as { kind: 'remote'; route: BrowserClientDownloadRoute }).route
|
||||
@@ -117,23 +105,6 @@ describe('BrowserClientDownloadRelay', () => {
|
||||
expect(removed).toContain(path.dirname(route.stagingPath))
|
||||
})
|
||||
|
||||
it('removes the per-transfer staging directory when the transfer is aborted', async () => {
|
||||
const { filesystem, removed } = memoryFilesystem(Buffer.from('abc'))
|
||||
const relay = new BrowserClientDownloadRelay({
|
||||
stagingRoot: '/tmp/staging',
|
||||
hostLabel: 'build-box',
|
||||
transport: negotiatedTransport(() => ({ released: true })),
|
||||
resolvePage: () => page,
|
||||
filesystem
|
||||
})
|
||||
|
||||
const route = remoteRoute(relay.route({ guestWebContentsId: 7 }))
|
||||
await route.abort()
|
||||
|
||||
expect(removed).toEqual([path.dirname(route.stagingPath)])
|
||||
expect(path.dirname(route.stagingPath)).not.toBe('/tmp/staging')
|
||||
})
|
||||
|
||||
it('leaves nothing on disk under the real staging root once transfers settle', async () => {
|
||||
const root = await realpath(await mkdtemp(path.join(tmpdir(), 'orca-download-relay-')))
|
||||
const stagingRoot = path.join(root, 'downloads')
|
||||
@@ -165,42 +136,6 @@ describe('BrowserClientDownloadRelay', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the local fallback when the host never offered the file channel', () => {
|
||||
const relay = new BrowserClientDownloadRelay({
|
||||
stagingRoot: '/tmp/staging',
|
||||
hostLabel: 'build-box',
|
||||
transport: boundTransport('unsupported'),
|
||||
resolvePage: () => page,
|
||||
filesystem: memoryFilesystem(Buffer.alloc(0)).filesystem
|
||||
})
|
||||
|
||||
expect(relay.route({ guestWebContentsId: 7 })).toEqual({ kind: 'local-fallback' })
|
||||
})
|
||||
|
||||
it('reports an owned page whose channel is momentarily gone as unavailable', () => {
|
||||
const relay = new BrowserClientDownloadRelay({
|
||||
stagingRoot: '/tmp/staging',
|
||||
hostLabel: 'build-box',
|
||||
transport: boundTransport('unavailable'),
|
||||
resolvePage: () => page,
|
||||
filesystem: memoryFilesystem(Buffer.alloc(0)).filesystem
|
||||
})
|
||||
|
||||
expect(relay.route({ guestWebContentsId: 7 })).toEqual({ kind: 'unavailable' })
|
||||
})
|
||||
|
||||
it('claims no ownership of a WebContents that is not one of its pages', () => {
|
||||
const relay = new BrowserClientDownloadRelay({
|
||||
stagingRoot: '/tmp/staging',
|
||||
hostLabel: 'build-box',
|
||||
transport: negotiatedTransport(() => ({ accepted: true })),
|
||||
resolvePage: () => undefined,
|
||||
filesystem: memoryFilesystem(Buffer.alloc(0)).filesystem
|
||||
})
|
||||
|
||||
expect(relay.route({ guestWebContentsId: 7 })).toEqual({ kind: 'unowned' })
|
||||
})
|
||||
|
||||
it('stops the commit and never sends the final chunk once the transfer is aborted', async () => {
|
||||
const writes: { final: boolean }[] = []
|
||||
const aborts: unknown[] = []
|
||||
|
||||
@@ -1,45 +1,9 @@
|
||||
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { BrowserClientHostAttachParams } from '../../shared/browser-client-host-protocol'
|
||||
import { createBrowserClientHostAttachRequest } from './browser-client-host-attach-request'
|
||||
import { sameBrowserClientHostLeaseAuthority } from './browser-client-host-command-authority'
|
||||
import { BrowserClientFileChannelTransport } from './browser-client-file-channel-transport'
|
||||
import { PairedRuntimeBrowserClientHost } from './paired-runtime-browser-client-host'
|
||||
|
||||
const TEST_CONNECTION = pairedRuntimeConnectionFromOffer({
|
||||
endpoint: 'ws://127.0.0.1:1',
|
||||
deviceToken: 'token',
|
||||
publicKeyB64: 'key'
|
||||
})
|
||||
|
||||
const leaseOptions = {
|
||||
connection: TEST_CONNECTION,
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
browserHostClientId: 'host-1',
|
||||
hostCapabilities: ['webview'],
|
||||
pageCommandProtocolVersion: 1 as const,
|
||||
onPageCommand: () => ({ status: 'completed' }) as never
|
||||
}
|
||||
|
||||
describe('browser file channel negotiation', () => {
|
||||
it('requests the file channel only alongside the command protocol', () => {
|
||||
expect(
|
||||
createBrowserClientHostAttachRequest({
|
||||
...leaseOptions,
|
||||
fileChannelProtocolVersion: 1
|
||||
}).params.fileChannelProtocolVersion
|
||||
).toBe(1)
|
||||
|
||||
expect(
|
||||
createBrowserClientHostAttachRequest({
|
||||
...leaseOptions,
|
||||
onPageCommand: undefined,
|
||||
fileChannelProtocolVersion: 1
|
||||
}).params.fileChannelProtocolVersion
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects an attach that asks for the file channel without command negotiation', () => {
|
||||
expect(
|
||||
BrowserClientHostAttachParams.safeParse({
|
||||
@@ -51,37 +15,6 @@ describe('browser file channel negotiation', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the same authority when a reconnect renegotiates the file channel', () => {
|
||||
const negotiated = {
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
authorityEpoch: 'epoch-1',
|
||||
browserHostClientId: 'host-1',
|
||||
browserHostGeneration: 1,
|
||||
pageCommandProtocolVersion: 1 as const,
|
||||
fileChannelProtocolVersion: 1 as const
|
||||
}
|
||||
|
||||
expect(sameBrowserClientHostLeaseAuthority(negotiated, negotiated)).toBe(true)
|
||||
expect(
|
||||
sameBrowserClientHostLeaseAuthority(negotiated, {
|
||||
...negotiated,
|
||||
fileChannelProtocolVersion: undefined
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
sameBrowserClientHostLeaseAuthority(negotiated, {
|
||||
...negotiated,
|
||||
browserHostGeneration: 2
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
sameBrowserClientHostLeaseAuthority(negotiated, {
|
||||
...negotiated,
|
||||
pageCommandProtocolVersion: undefined
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('reports the channel unavailable until a negotiated lease binds it', async () => {
|
||||
const transport = new BrowserClientFileChannelTransport()
|
||||
expect(transport.available).toBe(false)
|
||||
@@ -116,38 +49,4 @@ describe('browser file channel negotiation', () => {
|
||||
expect(transport.available).toBe(false)
|
||||
expect(transport.availability).toBe('unavailable')
|
||||
})
|
||||
|
||||
it('separates a host that never offered the channel from one whose lease is gone', () => {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the test reads the host's private channel state; a renamed field fails the test.
|
||||
const host = new PairedRuntimeBrowserClientHost({
|
||||
connection: TEST_CONNECTION,
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
browserHostClientId: 'host-1',
|
||||
hostCapabilities: ['webview'],
|
||||
fileChannelProtocolVersion: 1,
|
||||
handler: () => Promise.reject(new Error('unused'))
|
||||
}) as unknown as {
|
||||
closed: boolean
|
||||
authority: { fileChannelProtocolVersion?: 1 } | null
|
||||
lease: {
|
||||
authority: { fileChannelProtocolVersion?: 1 } | null
|
||||
fileChannelActive: boolean
|
||||
}
|
||||
fileChannelAvailability: string
|
||||
}
|
||||
|
||||
expect(host.fileChannelAvailability).toBe('unavailable')
|
||||
|
||||
host.authority = { fileChannelProtocolVersion: undefined }
|
||||
expect(host.fileChannelAvailability).toBe('unsupported')
|
||||
|
||||
host.authority = { fileChannelProtocolVersion: 1 }
|
||||
host.lease.authority = { fileChannelProtocolVersion: 1 }
|
||||
// Match acceptAuthority’s lease update.
|
||||
host.lease.fileChannelActive = true
|
||||
expect(host.fileChannelAvailability).toBe('negotiated')
|
||||
|
||||
host.closed = true
|
||||
expect(host.fileChannelAvailability).toBe('unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { PairingOffer } from '../../shared/pairing'
|
||||
import { createBrowserClientHostAttachRequest } from './browser-client-host-attach-request'
|
||||
|
||||
const pairing = {
|
||||
v: 2,
|
||||
endpoint: 'ws://127.0.0.1:6768',
|
||||
deviceToken: 'device-token',
|
||||
publicKeyB64: 'public-key',
|
||||
pairedDeviceId: 'device-a',
|
||||
scope: 'runtime'
|
||||
} as PairingOffer
|
||||
|
||||
describe('browser client host attach request', () => {
|
||||
it('omits unencodable inventory without changing legacy page-command negotiation', () => {
|
||||
const onPageCommand = vi.fn(() => ({ status: 'completed' as const }))
|
||||
const attach = createBrowserClientHostAttachRequest({
|
||||
connection: pairedRuntimeConnectionFromOffer(pairing),
|
||||
authorityRuntimeId: 'runtime-a',
|
||||
browserHostClientId: 'host-a',
|
||||
hostCapabilities: ['webview'],
|
||||
pageCommandProtocolVersion: 1,
|
||||
onPageCommand,
|
||||
pageInventoryProtocolVersion: 1,
|
||||
leaseReconnectProtocolVersion: 1,
|
||||
getPageInventory: () => [
|
||||
{
|
||||
authorityRuntimeId: 'runtime-old',
|
||||
authorityEpoch: 'epoch-old',
|
||||
browserHostClientId: 'host-a',
|
||||
browserHostGeneration: 2,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 3,
|
||||
browserProfileId: '\0'.repeat(256),
|
||||
executionHostKey: 'native:runtime-old:1',
|
||||
state: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(attach.pageCommandProtocolVersion).toBe(1)
|
||||
expect(attach.pageInventoryProtocolVersion).toBeUndefined()
|
||||
expect(attach.params).toMatchObject({ pageCommandProtocolVersion: 1 })
|
||||
expect(attach.params).not.toHaveProperty('pageInventoryProtocolVersion')
|
||||
expect(attach.params).not.toHaveProperty('pageInventory')
|
||||
expect(attach.params).not.toHaveProperty('leaseReconnectProtocolVersion')
|
||||
})
|
||||
|
||||
it('includes reconnect negotiation only beside an encoded inventory snapshot', () => {
|
||||
const attach = createBrowserClientHostAttachRequest({
|
||||
connection: pairedRuntimeConnectionFromOffer(pairing),
|
||||
authorityRuntimeId: 'runtime-a',
|
||||
browserHostClientId: 'host-a',
|
||||
hostCapabilities: ['webview'],
|
||||
pageInventoryProtocolVersion: 1,
|
||||
leaseReconnectProtocolVersion: 1,
|
||||
getPageInventory: () => []
|
||||
})
|
||||
|
||||
expect(attach).toMatchObject({
|
||||
pageInventoryProtocolVersion: 1,
|
||||
leaseReconnectProtocolVersion: 1,
|
||||
params: {
|
||||
pageInventoryProtocolVersion: 1,
|
||||
pageInventory: [],
|
||||
leaseReconnectProtocolVersion: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,73 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE } from '../../shared/browser-client-host-protocol'
|
||||
import { isBrowserClientHostAuthorityReplaced } from './browser-client-host-authority-replacement'
|
||||
import { BrowserClientHostAuthorityReplacementWait } from './browser-client-host-authority-replacement-wait'
|
||||
|
||||
function errorWithCode(message: string, code: unknown): Error {
|
||||
return Object.assign(new Error(message), { code })
|
||||
}
|
||||
|
||||
describe('browser client host authority replacement', () => {
|
||||
it('recognizes the structured mismatch code a current runtime sends', () => {
|
||||
const rejected = errorWithCode(
|
||||
'lease attach named a retired runtime',
|
||||
BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE
|
||||
)
|
||||
|
||||
expect(isBrowserClientHostAuthorityReplaced(rejected)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes the mismatch when a new client talks to a runtime older than the typed code', () => {
|
||||
const rejected = new Error(BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE)
|
||||
|
||||
expect('code' in rejected).toBe(false)
|
||||
expect(isBrowserClientHostAuthorityReplaced(rejected)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a host error that carries an unrelated code and an unrelated message', () => {
|
||||
const unrelated = errorWithCode('browser host process exited', 'runtime_error')
|
||||
|
||||
expect(isBrowserClientHostAuthorityReplaced(unrelated)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-Error rejections, including a bare object carrying the mismatch code', () => {
|
||||
expect(isBrowserClientHostAuthorityReplaced(undefined)).toBe(false)
|
||||
expect(isBrowserClientHostAuthorityReplaced(null)).toBe(false)
|
||||
expect(isBrowserClientHostAuthorityReplaced(BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE)).toBe(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
isBrowserClientHostAuthorityReplaced({ code: BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a non-string code that a loose comparison could coerce', () => {
|
||||
expect(isBrowserClientHostAuthorityReplaced(errorWithCode('host rejected attach', 503))).toBe(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
isBrowserClientHostAuthorityReplaced(
|
||||
errorWithCode('host rejected attach', {
|
||||
toString: () => BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a message that merely contains the mismatch code inside a longer sentence', () => {
|
||||
const quoted = new Error(`failed: ${BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE} occurred`)
|
||||
|
||||
expect(isBrowserClientHostAuthorityReplaced(quoted)).toBe(false)
|
||||
})
|
||||
|
||||
it('pins the mismatch code string', () => {
|
||||
// Older clients compare their fallback error message to this exact wire value.
|
||||
expect(BROWSER_CLIENT_HOST_AUTHORITY_MISMATCH_CODE).toBe(
|
||||
'browser_client_host_authority_mismatch'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BrowserClientHostAuthorityReplacementWait', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => tmpdir() },
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() }
|
||||
}))
|
||||
|
||||
type CompositionOptions = {
|
||||
createExecutor: (
|
||||
input: unknown,
|
||||
options: {
|
||||
retainNetworkRoute: () => Promise<unknown>
|
||||
onPageUnavailable: (browserPageId: string, pageHostGeneration: number) => void
|
||||
}
|
||||
) => unknown
|
||||
}
|
||||
|
||||
const compositionOptions: CompositionOptions[] = []
|
||||
const recordedUrlParams: unknown[] = []
|
||||
|
||||
vi.mock('./paired-runtime-browser-client-host', () => ({
|
||||
PairedRuntimeBrowserClientHost: class {}
|
||||
}))
|
||||
|
||||
vi.mock('./browser-client-file-channel-transport', () => ({
|
||||
BrowserClientFileChannelTransport: class {
|
||||
bind(): void {}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./browser-client-page-command-executor', () => ({
|
||||
BrowserClientPageCommandExecutor: class {
|
||||
readonly recordPublishedPageUrl = (params: unknown): void => {
|
||||
recordedUrlParams.push(params)
|
||||
}
|
||||
|
||||
findPageByWebContentsId(): undefined {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./paired-runtime-browser-client-host-composition', () => ({
|
||||
PairedRuntimeBrowserClientHostComposition: class {
|
||||
constructor(options: CompositionOptions) {
|
||||
compositionOptions.push(options)
|
||||
}
|
||||
|
||||
start(): Promise<unknown> {
|
||||
return Promise.resolve({ authority: 'lease-a' })
|
||||
}
|
||||
|
||||
close(): Promise<boolean> {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
whenClosed(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const ENVIRONMENT_ID = 'environment-a'
|
||||
|
||||
const metadata = {
|
||||
browserHostClientId: 'host-a',
|
||||
browserHostGeneration: 3,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
revision: 2,
|
||||
url: 'https://example.internal/where-the-user-went',
|
||||
title: 'Where the user went',
|
||||
loading: false,
|
||||
canGoBack: true,
|
||||
canGoForward: false
|
||||
}
|
||||
|
||||
function pairedEnvironment(): KnownRuntimeEnvironment {
|
||||
return {
|
||||
id: ENVIRONMENT_ID,
|
||||
name: 'Environment A',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
preferredEndpointId: 'endpoint-a',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'endpoint-a',
|
||||
endpoint: 'ws://127.0.0.1:9999',
|
||||
deviceToken: 'token-a',
|
||||
publicKeyB64: 'key-a'
|
||||
}
|
||||
]
|
||||
} as KnownRuntimeEnvironment
|
||||
}
|
||||
|
||||
async function startHost() {
|
||||
vi.resetModules()
|
||||
compositionOptions.length = 0
|
||||
recordedUrlParams.length = 0
|
||||
const runtime = await import('./paired-runtime-browser-client-host-runtime')
|
||||
runtime.configurePairedRuntimeBrowserClientHostsForOrcaProfile({ orcaProfileId: 'profile-a' })
|
||||
await runtime.startPairedRuntimeBrowserClientHost({
|
||||
environment: pairedEnvironment(),
|
||||
authorityRuntimeId: 'runtime-a'
|
||||
})
|
||||
const options = compositionOptions[0]
|
||||
if (!options) {
|
||||
throw new Error('client host composition was never constructed')
|
||||
}
|
||||
const { publishBrowserClientPageMetadata } =
|
||||
await import('./browser-client-page-metadata-transport')
|
||||
return { options, publishBrowserClientPageMetadata }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('published-url observation wiring', () => {
|
||||
// Adoption restores inventory URLs, so user navigations must reach this executor.
|
||||
it('routes a page-metadata publish into the executor that owns the page', async () => {
|
||||
const { options, publishBrowserClientPageMetadata } = await startHost()
|
||||
options.createExecutor(
|
||||
{ orcaProfileId: 'profile-a' },
|
||||
{ retainNetworkRoute: () => Promise.resolve({}), onPageUnavailable: () => {} }
|
||||
)
|
||||
|
||||
await publishBrowserClientPageMetadata(ENVIRONMENT_ID, metadata).catch(() => undefined)
|
||||
|
||||
expect(recordedUrlParams).toEqual([metadata])
|
||||
})
|
||||
})
|
||||
@@ -9,137 +9,6 @@ import {
|
||||
describe('BrowserClientPageCommandExecutor', () => {
|
||||
beforeEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('prepares the exact route and blank guest before granting navigation', async () => {
|
||||
const { dependencies, executor, order, renderer } = createHarness()
|
||||
const command = createCommand('createPage')
|
||||
|
||||
await expect(executor.handle(command, new AbortController().signal)).resolves.toEqual({
|
||||
status: 'completed'
|
||||
})
|
||||
|
||||
expect(order).toEqual([
|
||||
'retain-route',
|
||||
'prepare-page',
|
||||
'mount-page',
|
||||
'claim-guest',
|
||||
'register-guest',
|
||||
'grant-navigation',
|
||||
'bind-guest'
|
||||
])
|
||||
expect(dependencies.guestBinding.bind).toHaveBeenCalledWith({
|
||||
registration: {
|
||||
partition,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
rendererWebContentsId: 11,
|
||||
webContentsId: 41
|
||||
},
|
||||
browserProfileId: 'profile-a'
|
||||
})
|
||||
expect(dependencies.retainNetworkRoute).toHaveBeenCalledWith(
|
||||
'execution-host-a',
|
||||
expect.any(AbortSignal)
|
||||
)
|
||||
expect(dependencies.routeSessions.preparePage).toHaveBeenCalledWith({
|
||||
identity: {
|
||||
orcaProfileId: 'orca-profile-a',
|
||||
browserProfileId: 'profile-a',
|
||||
authorityConnectionIdentity: 'authority-record-a',
|
||||
executionHostIdentity: 'execution-host-record-a'
|
||||
},
|
||||
compatibleIdentity: {
|
||||
orcaProfileId: 'orca-profile-a',
|
||||
browserProfileId: 'profile-a',
|
||||
authorityConnectionIdentity: 'pairing-authority-record-a',
|
||||
executionHostIdentity: 'execution-host-record-a'
|
||||
},
|
||||
legacyIdentity: {
|
||||
orcaProfileId: 'orca-profile-a',
|
||||
browserProfileId: 'profile-a',
|
||||
authorityConnectionIdentity: 'legacy-authority-record-a',
|
||||
executionHostIdentity: 'legacy-execution-host-record-a'
|
||||
},
|
||||
storageScope: 'a'.repeat(64),
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
rendererWebContentsId: 11,
|
||||
proxyEndpoint: { host: '127.0.0.1', port: 43123 }
|
||||
})
|
||||
expect(renderer.mountPage).toHaveBeenCalledWith(
|
||||
{
|
||||
partition,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7
|
||||
},
|
||||
expect.any(AbortSignal)
|
||||
)
|
||||
expect(dependencies.routeWebContents.registerGuest).toHaveBeenCalledWith({
|
||||
partition,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
rendererWebContentsId: 11,
|
||||
webContentsId: 41
|
||||
})
|
||||
})
|
||||
|
||||
it('loads a normalized URL only through the retained exact guest', async () => {
|
||||
const { dependencies, executor } = createHarness()
|
||||
await executor.handle(createCommand('createPage'), new AbortController().signal)
|
||||
const lifecycleClaim = dependencies.routeWebContents.claimGuestLifecycle.mock.results[0]?.value
|
||||
|
||||
await expect(
|
||||
executor.handle(createCommand('navigate'), new AbortController().signal)
|
||||
).resolves.toEqual({ status: 'completed' })
|
||||
|
||||
expect(dependencies.routeWebContents.navigateGuest).toHaveBeenCalledWith(
|
||||
lifecycleClaim,
|
||||
'https://example.internal/path'
|
||||
)
|
||||
expect(executor.snapshotPageInventory()).toEqual([
|
||||
{
|
||||
authorityRuntimeId: 'runtime-a',
|
||||
authorityEpoch: 'epoch-a',
|
||||
browserHostClientId: 'client-a',
|
||||
browserHostGeneration: 3,
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
browserProfileId: 'profile-a',
|
||||
executionHostKey: 'execution-host-a',
|
||||
state: 'active',
|
||||
currentUrl: 'https://example.internal/path'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('executes one automation envelope against the exact retained page', async () => {
|
||||
const { dependencies, executor } = createHarness()
|
||||
await executor.handle(createCommand('createPage'), new AbortController().signal)
|
||||
const command = createCommand('navigate', {
|
||||
commandId: 'automation-a',
|
||||
command: {
|
||||
type: 'automation',
|
||||
method: 'browser.click',
|
||||
params: { element: 'Submit' }
|
||||
}
|
||||
})
|
||||
|
||||
await expect(executor.handle(command, new AbortController().signal)).resolves.toEqual({
|
||||
status: 'completed',
|
||||
value: { clicked: true }
|
||||
})
|
||||
expect(dependencies.executeAutomation).toHaveBeenCalledWith(
|
||||
{
|
||||
browserPageId: 'page-a',
|
||||
pageHostGeneration: 7,
|
||||
browserProfileId: 'profile-a',
|
||||
method: 'browser.click',
|
||||
params: { element: 'Submit' },
|
||||
registration: expect.objectContaining({ browserPageId: 'page-a', webContentsId: 41 })
|
||||
},
|
||||
expect.any(AbortSignal)
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps legacy-valid command identities when inventory cannot encode them', async () => {
|
||||
const { executor } = createHarness()
|
||||
const browserProfileId = '\0'.repeat(256)
|
||||
@@ -605,8 +474,4 @@ describe('BrowserClientPageCommandExecutor', () => {
|
||||
resolveRoute(route)
|
||||
await expect(first).resolves.toEqual({ status: 'completed' })
|
||||
})
|
||||
|
||||
it('rejects a page limit above the attach inventory maximum', () => {
|
||||
expect(() => createHarness({ maxPages: 257 })).toThrow('browser_client_page_limit_invalid')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from '../../shared/remote-runtime-client-error'
|
||||
import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
|
||||
import { BROWSER_CLIENT_FILE_CHANNEL_REQUEST_TIMEOUT_MS } from './browser-client-file-channel-transport'
|
||||
import {
|
||||
BROWSER_CLIENT_PAGE_METADATA_REQUEST_TIMEOUT_MS,
|
||||
BrowserClientPageMetadataTransport,
|
||||
publishBrowserClientPageMetadata,
|
||||
registerBrowserClientPageMetadataTransport,
|
||||
@@ -36,31 +34,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('browser client page metadata transport', () => {
|
||||
it('waits at least as long as anything else sharing the lease connection', () => {
|
||||
expect(BROWSER_CLIENT_PAGE_METADATA_REQUEST_TIMEOUT_MS).toBeGreaterThanOrEqual(
|
||||
BROWSER_CLIENT_FILE_CHANNEL_REQUEST_TIMEOUT_MS
|
||||
)
|
||||
expect(BROWSER_CLIENT_PAGE_METADATA_REQUEST_TIMEOUT_MS).toBeGreaterThanOrEqual(30_000)
|
||||
})
|
||||
|
||||
it('sends the publish over the bound lease and reports the acknowledgement', async () => {
|
||||
const sendPageMetadataRequest = vi.fn().mockResolvedValue(answered({ accepted: true }))
|
||||
const transport = new BrowserClientPageMetadataTransport()
|
||||
transport.bind({ sendPageMetadataRequest })
|
||||
|
||||
await expect(transport.publish(PARAMS)).resolves.toEqual({ accepted: true })
|
||||
expect(sendPageMetadataRequest).toHaveBeenCalledWith(PARAMS, expect.any(Number))
|
||||
})
|
||||
|
||||
it('carries an un-accepted acknowledgement back to the caller', async () => {
|
||||
const transport = new BrowserClientPageMetadataTransport()
|
||||
transport.bind({
|
||||
sendPageMetadataRequest: () => Promise.resolve(answered({ accepted: false }))
|
||||
})
|
||||
|
||||
await expect(transport.publish(PARAMS)).resolves.toEqual({ accepted: false })
|
||||
})
|
||||
|
||||
it('rejects a runtime error and an unreadable acknowledgement', async () => {
|
||||
const failing = new BrowserClientPageMetadataTransport()
|
||||
failing.bind({
|
||||
@@ -148,29 +121,3 @@ describe('browser client page metadata routing', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('published-url observation', () => {
|
||||
it('reports every publish to the observer', async () => {
|
||||
const observeCurrentUrl = vi.fn()
|
||||
const transport = new BrowserClientPageMetadataTransport(observeCurrentUrl)
|
||||
transport.bind({
|
||||
sendPageMetadataRequest: vi.fn().mockResolvedValue(answered({ accepted: true }))
|
||||
})
|
||||
|
||||
await transport.publish(PARAMS)
|
||||
await transport.publish({ ...PARAMS, revision: 3, url: 'https://example.internal/again' })
|
||||
|
||||
expect(observeCurrentUrl.mock.calls).toEqual([
|
||||
[PARAMS],
|
||||
[{ ...PARAMS, revision: 3, url: 'https://example.internal/again' }]
|
||||
])
|
||||
})
|
||||
|
||||
it('observes the publish even when no lease is bound to send it', async () => {
|
||||
const observeCurrentUrl = vi.fn()
|
||||
const transport = new BrowserClientPageMetadataTransport(observeCurrentUrl)
|
||||
|
||||
await expect(transport.publish(PARAMS)).rejects.toBeInstanceOf(RemoteRuntimeClientError)
|
||||
expect(observeCurrentUrl).toHaveBeenCalledWith(PARAMS)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
BROWSER_CLIENT_HOST_PAGE_INVENTORY_URL_MAX_LENGTH,
|
||||
type BrowserClientHostedPageInventory
|
||||
} from '../../shared/browser-client-host-protocol'
|
||||
import { BrowserClientPageMetadataParams } from '../../shared/browser-client-page-metadata-protocol'
|
||||
import { recordBrowserClientPagePublishedUrl } from './browser-client-page-inventory'
|
||||
|
||||
const HOST_CLIENT_ID = 'client-a'
|
||||
const PAGE_ID = 'page-a'
|
||||
const PAGE_HOST_GENERATION = 4
|
||||
|
||||
const inventory = (
|
||||
overrides: Partial<BrowserClientHostedPageInventory> = {}
|
||||
): BrowserClientHostedPageInventory => ({
|
||||
authorityRuntimeId: 'runtime-a',
|
||||
authorityEpoch: 'epoch-a',
|
||||
browserHostClientId: HOST_CLIENT_ID,
|
||||
browserHostGeneration: 3,
|
||||
browserPageId: PAGE_ID,
|
||||
pageHostGeneration: PAGE_HOST_GENERATION,
|
||||
browserProfileId: 'profile-a',
|
||||
executionHostKey: `native:runtime-a:1`,
|
||||
state: 'active',
|
||||
...overrides
|
||||
})
|
||||
|
||||
const tracked = (
|
||||
entry: BrowserClientHostedPageInventory = inventory(),
|
||||
generation = PAGE_HOST_GENERATION
|
||||
) => new Map([[entry.browserPageId, { generation, inventory: entry }]])
|
||||
|
||||
const metadata = (overrides: Partial<BrowserClientPageMetadataParams> = {}) => ({
|
||||
browserHostClientId: HOST_CLIENT_ID,
|
||||
browserHostGeneration: 3,
|
||||
browserPageId: PAGE_ID,
|
||||
pageHostGeneration: PAGE_HOST_GENERATION,
|
||||
revision: 1,
|
||||
url: 'https://remote.example/after-navigation',
|
||||
title: 'After navigation',
|
||||
loading: false,
|
||||
canGoBack: true,
|
||||
canGoForward: false,
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('recordBrowserClientPagePublishedUrl', () => {
|
||||
it('records the published url on the matching page', () => {
|
||||
const pages = tracked()
|
||||
|
||||
recordBrowserClientPagePublishedUrl(pages, metadata())
|
||||
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toBe('https://remote.example/after-navigation')
|
||||
})
|
||||
|
||||
it('ignores metadata whose pageHostGeneration does not match the tracked page', () => {
|
||||
const pages = tracked(inventory({ currentUrl: 'https://remote.example/opened-at' }))
|
||||
|
||||
recordBrowserClientPagePublishedUrl(pages, metadata({ pageHostGeneration: 99 }))
|
||||
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toBe('https://remote.example/opened-at')
|
||||
})
|
||||
|
||||
it('ignores metadata for a page id the map does not track', () => {
|
||||
const pages = tracked(inventory({ currentUrl: 'https://remote.example/opened-at' }))
|
||||
|
||||
expect(() =>
|
||||
recordBrowserClientPagePublishedUrl(pages, metadata({ browserPageId: 'page-unknown' }))
|
||||
).not.toThrow()
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toBe('https://remote.example/opened-at')
|
||||
expect(pages.has('page-unknown')).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['an empty object', {}],
|
||||
['undefined', undefined],
|
||||
['a string', 'browser.clientHost.pageMetadata'],
|
||||
['a non-string url', metadata({ url: 42 as unknown as string })],
|
||||
['a generation below the schema floor', metadata({ pageHostGeneration: 0 })]
|
||||
])('ignores params that fail schema validation: %s', (_label, params) => {
|
||||
const pages = tracked(inventory({ currentUrl: 'https://remote.example/opened-at' }))
|
||||
|
||||
expect(() => recordBrowserClientPagePublishedUrl(pages, params)).not.toThrow()
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toBe('https://remote.example/opened-at')
|
||||
})
|
||||
|
||||
it('stores a url exactly at the inventory url cap', () => {
|
||||
const pages = tracked()
|
||||
const url = `https://remote.example/${'a'.repeat(
|
||||
BROWSER_CLIENT_HOST_PAGE_INVENTORY_URL_MAX_LENGTH - 'https://remote.example/'.length
|
||||
)}`
|
||||
|
||||
recordBrowserClientPagePublishedUrl(pages, metadata({ url }))
|
||||
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toHaveLength(
|
||||
BROWSER_CLIENT_HOST_PAGE_INVENTORY_URL_MAX_LENGTH
|
||||
)
|
||||
})
|
||||
|
||||
// The metadata schema rejects oversized URLs before the inventory’s URL-drop fallback.
|
||||
it('rejects an over-cap url at schema validation rather than recording it', () => {
|
||||
const pages = tracked(inventory({ currentUrl: 'https://remote.example/opened-at' }))
|
||||
const url = 'h'.repeat(BROWSER_CLIENT_HOST_PAGE_INVENTORY_URL_MAX_LENGTH + 1)
|
||||
|
||||
expect(BrowserClientPageMetadataParams.safeParse(metadata({ url })).success).toBe(false)
|
||||
|
||||
recordBrowserClientPagePublishedUrl(pages, metadata({ url }))
|
||||
|
||||
expect(pages.get(PAGE_ID)?.inventory.currentUrl).toBe('https://remote.example/opened-at')
|
||||
})
|
||||
})
|
||||
@@ -248,32 +248,6 @@ describe('browser client page reconciliation adapters', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('restores a host-owned profile with the metadata the host sent', async () => {
|
||||
const { executor, dependencies } = createHarness()
|
||||
const profile = {
|
||||
id: 'remote-profile',
|
||||
label: 'Remote profile',
|
||||
scope: 'isolated' as const
|
||||
}
|
||||
const restore = command('restorePage', {
|
||||
command: {
|
||||
type: 'restorePage',
|
||||
browserProfileId: profile.id,
|
||||
profile,
|
||||
executionHostKey: 'execution-host-a'
|
||||
}
|
||||
})
|
||||
await expect(executor.handle(restore, new AbortController().signal)).resolves.toEqual({
|
||||
status: 'completed'
|
||||
})
|
||||
expect(dependencies.routeSessions.preparePage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
identity: expect.objectContaining({ browserProfileId: profile.id }),
|
||||
profileMetadata: profile
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('destroys a rekeyed guest instead of restoring authority after renderer failure', async () => {
|
||||
const { executor, renderer, routeWebContents } = createHarness()
|
||||
await executor.handle(command('createPage'), new AbortController().signal)
|
||||
|
||||
@@ -41,16 +41,6 @@ describe('browser client page unavailability', () => {
|
||||
expect(renderer.retirePage).not.toHaveBeenCalled()
|
||||
expect(executor.snapshotPageInventory()).toEqual([])
|
||||
})
|
||||
|
||||
it('releases the page availability watch when the page retires', async () => {
|
||||
const { executor, releaseAvailabilityWatch } = harness()
|
||||
await executor.handle(createCommand(), new AbortController().signal)
|
||||
expect(releaseAvailabilityWatch).not.toHaveBeenCalled()
|
||||
|
||||
await expect(executor.retirePage('page-a', 7)).resolves.toBe(true)
|
||||
|
||||
expect(releaseAvailabilityWatch).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
function harness() {
|
||||
|
||||
@@ -6,10 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { BrowserClientHostCommandEvent } from '../../shared/browser-client-host-protocol'
|
||||
import { BrowserClientFileChannelTransport } from './browser-client-file-channel-transport'
|
||||
import { BrowserClientPageCommandExecutor } from './browser-client-page-command-executor'
|
||||
import {
|
||||
BROWSER_CLIENT_UPLOAD_STAGING_MAX_COMMANDS_PER_PAGE,
|
||||
BrowserClientUploadStaging
|
||||
} from './browser-client-upload-staging'
|
||||
import { BrowserClientUploadStaging } from './browser-client-upload-staging'
|
||||
import type { BrowserRoutePageGuestIdentity } from './browser-route-page-authority'
|
||||
|
||||
const partition = `persist:orca-browser-v1-${'a'.repeat(64)}`
|
||||
@@ -153,23 +150,6 @@ function negotiatedTransport(contents: string): BrowserClientFileChannelTranspor
|
||||
}
|
||||
|
||||
describe('client-placed browser.upload routing', () => {
|
||||
it('never forwards the remote paths verbatim to the local automation runtime', async () => {
|
||||
const transport = negotiatedTransport('remote-bytes')
|
||||
const { executor, automationCalls } = createHarness({ fileChannel: transport })
|
||||
await executor.handle(createPage, new AbortController().signal)
|
||||
|
||||
const result = await executor.handle(
|
||||
uploadCommand(['docs/report.pdf']),
|
||||
new AbortController().signal
|
||||
)
|
||||
|
||||
expect(result).toEqual({ status: 'completed', value: { uploaded: true } })
|
||||
const forwarded = automationCalls[0].params.files as string[]
|
||||
expect(forwarded).not.toContain('docs/report.pdf')
|
||||
expect(path.basename(forwarded[0])).toBe('report.pdf')
|
||||
expect(forwarded[0].startsWith(stagingRoot)).toBe(true)
|
||||
})
|
||||
|
||||
it('fails the command instead of resolving remote paths on this desktop when unnegotiated', async () => {
|
||||
const { executor, executeAutomation } = createHarness()
|
||||
await executor.handle(createPage, new AbortController().signal)
|
||||
@@ -202,26 +182,6 @@ describe('client-placed browser.upload routing', () => {
|
||||
expect(await readdir(stagingRoot)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('evicts a page oldest-first once it exceeds the staged-command budget', async () => {
|
||||
const transport = negotiatedTransport('remote-bytes')
|
||||
const { executor, uploadStaging } = createHarness({ fileChannel: transport })
|
||||
await executor.handle(createPage, new AbortController().signal)
|
||||
|
||||
for (let sequence = 2; sequence <= 8; sequence += 1) {
|
||||
await executor.handle(
|
||||
uploadCommand(['docs/report.pdf'], sequence),
|
||||
new AbortController().signal
|
||||
)
|
||||
}
|
||||
|
||||
expect(uploadStaging.activeStagingCount()).toBe(
|
||||
BROWSER_CLIENT_UPLOAD_STAGING_MAX_COMMANDS_PER_PAGE
|
||||
)
|
||||
expect(await readdir(stagingRoot)).toHaveLength(
|
||||
BROWSER_CLIENT_UPLOAD_STAGING_MAX_COMMANDS_PER_PAGE
|
||||
)
|
||||
})
|
||||
|
||||
it('retires the guest and releases the route when the staged copies cannot be removed', async () => {
|
||||
const transport = negotiatedTransport('remote-bytes')
|
||||
const { executor, retireRendererPage, releaseRouteSession, releaseNetworkRoute } =
|
||||
|
||||
@@ -88,31 +88,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('importCookiesIntoClientRoutePartition', () => {
|
||||
it('imports into a main-derived route partition, never the server profile partition', async () => {
|
||||
const result = await importCookiesIntoClientRoutePartition(request)
|
||||
|
||||
expect(result).toMatchObject({ ok: true, profileId: 'default' })
|
||||
expect(requireRouteBrowserProfileMock).toHaveBeenCalledWith('default')
|
||||
const [source, partition] = importCookiesFromBrowserMock.mock.calls[0] ?? []
|
||||
expect(source).toBe(chrome)
|
||||
expect(partition).toMatch(/^persist:orca-browser-v1-[a-f0-9]{64}$/)
|
||||
expect(bindingStore.set).toHaveBeenCalledWith(
|
||||
partition,
|
||||
expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
routeIdentity.storageScope,
|
||||
expect.stringMatching(/^[a-f0-9]{64}$/)
|
||||
)
|
||||
expect(recordClientRouteImportSourceMock).toHaveBeenCalledWith({
|
||||
environmentId: 'env-a',
|
||||
profileId: 'default',
|
||||
source: {
|
||||
browserFamily: 'chrome',
|
||||
profileName: 'Person 1',
|
||||
importedAt: expect.any(Number)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('defers to the server-side import when the desktop hosts no pages for the server', async () => {
|
||||
getRouteIdentityMock.mockReturnValue(null)
|
||||
|
||||
@@ -137,16 +112,6 @@ describe('importCookiesIntoClientRoutePartition', () => {
|
||||
expect(detectInstalledBrowsersMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reselects a non-default desktop browser profile', async () => {
|
||||
const secondary = { ...chrome, selectedProfile: 'Profile 2' }
|
||||
selectBrowserProfileMock.mockReturnValue(secondary)
|
||||
|
||||
await importCookiesIntoClientRoutePartition({ ...request, browserProfile: 'Profile 2' })
|
||||
|
||||
expect(selectBrowserProfileMock).toHaveBeenCalledWith(chrome, 'Profile 2')
|
||||
expect(importCookiesFromBrowserMock.mock.calls[0]?.[0]).toBe(secondary)
|
||||
})
|
||||
|
||||
it('leaves the profile source untouched when the import fails', async () => {
|
||||
importCookiesFromBrowserMock.mockResolvedValue({ ok: false, reason: 'locked database' })
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, readFile, readdir, realpath, rm } from 'node:fs/promises'
|
||||
import { mkdtemp, realpath, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { BrowserClientHostCommandEvent } from '../../shared/browser-client-host-protocol'
|
||||
import { BrowserClientFileChannelTransport } from './browser-client-file-channel-transport'
|
||||
@@ -62,79 +61,6 @@ function transportReturning(
|
||||
}
|
||||
|
||||
describe('executeBrowserClientUploadCommand', () => {
|
||||
it('rewrites remote paths to staged copies that outlive the command until the page is released', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot)
|
||||
const { transport, calls } = transportReturning(
|
||||
new Map([
|
||||
[
|
||||
'docs/report.pdf',
|
||||
[{ contentBase64: Buffer.from('hello').toString('base64'), bytesRead: 5, eof: true }]
|
||||
]
|
||||
])
|
||||
)
|
||||
const run = vi.fn().mockResolvedValue({ uploaded: true })
|
||||
|
||||
const result = await executeBrowserClientUploadCommand({
|
||||
event: uploadEvent(['docs/report.pdf']),
|
||||
params: { element: '#f', files: ['docs/report.pdf'] },
|
||||
fileChannel: transport,
|
||||
staging,
|
||||
run
|
||||
})
|
||||
|
||||
expect(result).toEqual({ uploaded: true })
|
||||
const rewritten = run.mock.calls[0][0] as { files: string[] }
|
||||
expect(rewritten.files).toHaveLength(1)
|
||||
expect(path.basename(rewritten.files[0])).toBe('report.pdf')
|
||||
expect(rewritten.files[0]).not.toBe('docs/report.pdf')
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(await readFile(rewritten.files[0], 'utf8')).toBe('hello')
|
||||
expect(staging.activeStagingCount()).toBe(1)
|
||||
|
||||
expect(await staging.releasePage('page-1')).toBe(1)
|
||||
expect(await readdir(stagingRoot)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('fails closed and stages nothing when the file channel was not negotiated', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot)
|
||||
const transport = new BrowserClientFileChannelTransport()
|
||||
const run = vi.fn()
|
||||
|
||||
await expect(
|
||||
executeBrowserClientUploadCommand({
|
||||
event: uploadEvent(['/etc/passwd']),
|
||||
params: { element: '#f', files: ['/etc/passwd'] },
|
||||
fileChannel: transport,
|
||||
staging,
|
||||
run
|
||||
})
|
||||
).rejects.toThrow('browser_client_file_channel_unsupported')
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
expect(staging.activeStagingCount()).toBe(0)
|
||||
expect(existsSync(stagingRoot)).toBe(false)
|
||||
})
|
||||
|
||||
it('removes staged files when the upload itself fails', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot)
|
||||
const { transport } = transportReturning(
|
||||
new Map([['a.txt', [{ contentBase64: '', bytesRead: 0, eof: true }]]])
|
||||
)
|
||||
|
||||
await expect(
|
||||
executeBrowserClientUploadCommand({
|
||||
event: uploadEvent(['a.txt']),
|
||||
params: { element: '#f', files: ['a.txt'] },
|
||||
fileChannel: transport,
|
||||
staging,
|
||||
run: async () => {
|
||||
throw new Error('element not found')
|
||||
}
|
||||
})
|
||||
).rejects.toThrow('element not found')
|
||||
expect(staging.activeStagingCount()).toBe(0)
|
||||
expect(await readdir(stagingRoot)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('reports the upload failure, not the cleanup failure, when both fail', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot, {
|
||||
mkdir: async () => {},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import type * as NodeFsPromises from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
BROWSER_CLIENT_UPLOAD_STAGING_MAX_BYTES_PER_PAGE,
|
||||
@@ -11,19 +10,6 @@ import {
|
||||
BrowserClientUploadStaging
|
||||
} from './browser-client-upload-staging'
|
||||
|
||||
const nodeRemovals = vi.hoisted(() => ({ rm: [] as { target: unknown; options: unknown }[] }))
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NodeFsPromises>()
|
||||
return {
|
||||
...actual,
|
||||
rm: (target: unknown, options: unknown) => {
|
||||
nodeRemovals.rm.push({ target, options })
|
||||
return (actual.rm as (t: unknown, o: unknown) => Promise<void>)(target, options)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let stagingRoot = ''
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -308,24 +294,4 @@ describe('BrowserClientUploadStaging', () => {
|
||||
// A later release needs the retained directory record to retry cleanup.
|
||||
expect(staging.activeStagingCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('retries the staged removal so a briefly held file is not orphaned', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot)
|
||||
const staged = await staging.stage({
|
||||
browserPageId: 'page-1',
|
||||
pageHostGeneration: 1,
|
||||
files: [{ remotePath: 'a.txt', contents: Buffer.from('a') }]
|
||||
})
|
||||
const directory = staging.stagedDirectory(staged.stagingId)
|
||||
nodeRemovals.rm.length = 0
|
||||
|
||||
expect(await staging.release(staged.stagingId)).toBe(true)
|
||||
|
||||
expect(nodeRemovals.rm).toEqual([
|
||||
{
|
||||
target: directory,
|
||||
options: { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { BrowserClientHostCommandEvent } from '../../shared/browser-client-host-protocol'
|
||||
import {
|
||||
BROWSER_CLIENT_FILE_CHANNEL_READ_METHOD,
|
||||
fetchBrowserClientUploadFiles,
|
||||
readBrowserClientUploadPaths
|
||||
} from './browser-client-upload-transfer'
|
||||
import { fetchBrowserClientUploadFiles } from './browser-client-upload-transfer'
|
||||
|
||||
const event = {
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
@@ -17,37 +13,6 @@ const event = {
|
||||
} as BrowserClientHostCommandEvent
|
||||
|
||||
describe('fetchBrowserClientUploadFiles', () => {
|
||||
it('reassembles a multi-chunk remote file and stamps the page authority on every read', async () => {
|
||||
const requests: { method: string; params: Record<string, unknown> }[] = []
|
||||
const chunks = ['abc', 'de']
|
||||
const files = await fetchBrowserClientUploadFiles({
|
||||
request: async (method, params) => {
|
||||
requests.push({ method, params: params as Record<string, unknown> })
|
||||
const chunk = chunks.shift() ?? ''
|
||||
return {
|
||||
contentBase64: Buffer.from(chunk).toString('base64'),
|
||||
bytesRead: chunk.length,
|
||||
totalBytes: 5,
|
||||
eof: chunks.length === 0
|
||||
}
|
||||
},
|
||||
event,
|
||||
remotePaths: ['docs/a.txt']
|
||||
})
|
||||
|
||||
expect(files).toEqual([{ remotePath: 'docs/a.txt', contents: Buffer.from('abcde') }])
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[0].method).toBe(BROWSER_CLIENT_FILE_CHANNEL_READ_METHOD)
|
||||
expect(requests[0].params).toMatchObject({
|
||||
fileChannelProtocolVersion: 1,
|
||||
browserPageId: 'page-1',
|
||||
pageHostGeneration: 3,
|
||||
workspaceRelativePath: 'docs/a.txt',
|
||||
offset: 0
|
||||
})
|
||||
expect(requests[1].params).toMatchObject({ offset: 3 })
|
||||
})
|
||||
|
||||
it('stops instead of looping forever on a host that never reports eof', async () => {
|
||||
await expect(
|
||||
fetchBrowserClientUploadFiles({
|
||||
@@ -83,22 +48,3 @@ describe('fetchBrowserClientUploadFiles', () => {
|
||||
).rejects.toThrow('browser_client_upload_file_count_exceeded')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readBrowserClientUploadPaths', () => {
|
||||
it('accepts a list of non-empty path strings', () => {
|
||||
expect(readBrowserClientUploadPaths({ files: ['a.txt', 'b/c.txt'] })).toEqual([
|
||||
'a.txt',
|
||||
'b/c.txt'
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects a missing, non-array, or non-string files payload', () => {
|
||||
expect(() => readBrowserClientUploadPaths({})).toThrow('browser_client_upload_files_required')
|
||||
expect(() => readBrowserClientUploadPaths({ files: 'a.txt' })).toThrow(
|
||||
'browser_client_upload_files_required'
|
||||
)
|
||||
expect(() => readBrowserClientUploadPaths({ files: [''] })).toThrow(
|
||||
'browser_client_upload_files_required'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
browserAuthorityExecutionHostStorageIdentity,
|
||||
browserNetworkExecutionHostStorageIdentity,
|
||||
legacyBrowserNativeExecutionHostStorageIdentity,
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity
|
||||
} from './browser-execution-host-storage-identity'
|
||||
import { browserNetworkExecutionHostKey } from './browser-network-execution-route'
|
||||
|
||||
const storageKey = 'a'.repeat(64)
|
||||
const otherStorageKey = 'b'.repeat(64)
|
||||
|
||||
describe('browserNetworkExecutionHostStorageIdentity', () => {
|
||||
it('ignores the per-boot components the route key fences on', () => {
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-a', revision: 1 },
|
||||
storageKey
|
||||
)
|
||||
).toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-a', revision: 2 },
|
||||
storageKey
|
||||
)
|
||||
)
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 1, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
)
|
||||
).toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 2, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
)
|
||||
)
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: 'ssh-1', providerEpoch: 'epoch-a', connectionGeneration: 1 },
|
||||
storageKey
|
||||
)
|
||||
).toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: 'ssh-1', providerEpoch: 'epoch-b', connectionGeneration: 2 },
|
||||
storageKey
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('survives the authority runtime restarting under a new runtimeId', () => {
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-a', revision: 1 },
|
||||
storageKey
|
||||
)
|
||||
).toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-b', revision: 9 },
|
||||
storageKey
|
||||
)
|
||||
)
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 1, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
)
|
||||
).toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-b', revision: 9, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('separates every boundary that changes storage or egress', () => {
|
||||
const identities = [
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-a', revision: 1 },
|
||||
storageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'runtime-a', revision: 1 },
|
||||
otherStorageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 1, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 1, distro: 'Debian' },
|
||||
storageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'runtime-a', revision: 1, distro: 'Ubuntu' },
|
||||
otherStorageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: 'ssh-1', providerEpoch: 'epoch-a', connectionGeneration: 1 },
|
||||
storageKey
|
||||
),
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: 'ssh-2', providerEpoch: 'epoch-a', connectionGeneration: 1 },
|
||||
storageKey
|
||||
)
|
||||
]
|
||||
|
||||
expect(new Set(identities).size).toBe(identities.length)
|
||||
})
|
||||
|
||||
it('keeps delimiter-bearing components structurally distinct', () => {
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'a', revision: 1, distro: 'b","c' },
|
||||
'a'
|
||||
)
|
||||
).not.toBe(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'a', revision: 1, distro: 'c' },
|
||||
'a","b'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// These serialized identities name persisted partitions, including migration sources.
|
||||
it('pins the storage identity of every host kind, current and pre-migration', () => {
|
||||
const tag = '"orca-browser-execution-host-storage",1'
|
||||
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'native', runtimeId: 'r', revision: 1 },
|
||||
storageKey
|
||||
)
|
||||
).toBe(`[${tag},"authority","${storageKey}"]`)
|
||||
expect(browserAuthorityExecutionHostStorageIdentity(storageKey)).toBe(
|
||||
`[${tag},"authority","${storageKey}"]`
|
||||
)
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'wsl', runtimeId: 'r', revision: 1, distro: 'Ubuntu' },
|
||||
storageKey
|
||||
)
|
||||
).toBe(`[${tag},"authority-wsl","${storageKey}","Ubuntu"]`)
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: 'ssh-1', providerEpoch: 'e', connectionGeneration: 1 },
|
||||
storageKey
|
||||
)
|
||||
).toBe(`[${tag},"ssh","ssh-1"]`)
|
||||
|
||||
expect(legacyBrowserNativeExecutionHostStorageIdentity('r')).toBe(`[${tag},"native","r"]`)
|
||||
expect(
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity({
|
||||
kind: 'native',
|
||||
runtimeId: 'r',
|
||||
revision: 1
|
||||
})
|
||||
).toBe(`[${tag},"native","r"]`)
|
||||
expect(
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity({
|
||||
kind: 'wsl',
|
||||
runtimeId: 'r',
|
||||
revision: 1,
|
||||
distro: 'Ubuntu'
|
||||
})
|
||||
).toBe(`[${tag},"wsl","r","Ubuntu"]`)
|
||||
expect(
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity({
|
||||
kind: 'ssh',
|
||||
targetId: 'ssh-1',
|
||||
providerEpoch: 'e',
|
||||
connectionGeneration: 1
|
||||
})
|
||||
).toBe(`[${tag},"ssh","ssh-1"]`)
|
||||
})
|
||||
|
||||
it('keeps pre-migration WSL distros apart on one runtime', () => {
|
||||
expect(
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity({
|
||||
kind: 'wsl',
|
||||
runtimeId: 'r',
|
||||
revision: 1,
|
||||
distro: 'Ubuntu'
|
||||
})
|
||||
).not.toBe(
|
||||
legacyBrowserNetworkExecutionHostStorageIdentity({
|
||||
kind: 'wsl',
|
||||
runtimeId: 'r',
|
||||
revision: 1,
|
||||
distro: 'Debian'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('never lets an SSH target share the paired server machine identity', () => {
|
||||
const shared = 'ssh-1700000000-aaa111'
|
||||
|
||||
expect(
|
||||
browserNetworkExecutionHostStorageIdentity(
|
||||
{ kind: 'ssh', targetId: shared, providerEpoch: 'e', connectionGeneration: 1 },
|
||||
shared
|
||||
)
|
||||
).not.toBe(browserAuthorityExecutionHostStorageIdentity(shared))
|
||||
})
|
||||
|
||||
it('is never mistaken for a route fencing key', () => {
|
||||
const host = {
|
||||
kind: 'native',
|
||||
runtimeId: 'runtime-a',
|
||||
revision: 7
|
||||
} as const
|
||||
|
||||
expect(browserNetworkExecutionHostStorageIdentity(host, storageKey)).not.toBe(
|
||||
browserNetworkExecutionHostKey(host)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -127,11 +127,6 @@ describe('browser external app launch', () => {
|
||||
expect(renderer.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('names no site when the asking frame is unknown', () => {
|
||||
ask(OKTA_URL, null)
|
||||
expect(sentRequests(renderer)[0].details).toMatchObject({ origin: null })
|
||||
})
|
||||
|
||||
it('refuses a page with no window on this computer', () => {
|
||||
resolveTarget = () => null
|
||||
expect(ask()).toBe(false)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
googleAuthUserAgent,
|
||||
isGoogleAuthUrl,
|
||||
setUserAgentHeader,
|
||||
stripClientHints
|
||||
} from './browser-google-auth-ua'
|
||||
|
||||
describe('isGoogleAuthUrl', () => {
|
||||
it('matches the Google/YouTube sign-in hosts exactly', () => {
|
||||
expect(isGoogleAuthUrl('https://accounts.google.com/')).toBe(true)
|
||||
expect(isGoogleAuthUrl('https://accounts.google.com/v3/signin/identifier')).toBe(true)
|
||||
expect(isGoogleAuthUrl('https://accounts.youtube.com/signin')).toBe(true)
|
||||
expect(isGoogleAuthUrl('https://ACCOUNTS.GOOGLE.COM/')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match post-auth app subdomains or lookalikes', () => {
|
||||
expect(isGoogleAuthUrl('https://myaccount.google.com/')).toBe(false)
|
||||
expect(isGoogleAuthUrl('https://mail.google.com/')).toBe(false)
|
||||
expect(isGoogleAuthUrl('https://accounts.google.com.evil.test/')).toBe(false)
|
||||
expect(isGoogleAuthUrl('https://google.com/')).toBe(false)
|
||||
expect(isGoogleAuthUrl('not a url')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('googleAuthUserAgent', () => {
|
||||
it('produces an internally consistent Firefox UA for the host platform', () => {
|
||||
const ua = googleAuthUserAgent()
|
||||
expect(ua).toMatch(/^Mozilla\/5\.0 \(.+; rv:\d+\.0\) Gecko\/20100101 Firefox\/\d+\.0$/)
|
||||
expect(ua).not.toContain('Chrome')
|
||||
expect(ua).not.toContain('Electron')
|
||||
})
|
||||
})
|
||||
|
||||
describe('stripClientHints', () => {
|
||||
it('removes every sec-ch-ua* header regardless of case, keeps others', () => {
|
||||
const headers: Record<string, string> = {
|
||||
'sec-ch-ua': 'a',
|
||||
'Sec-CH-UA-Platform': 'b',
|
||||
'sec-ch-ua-full-version-list': 'c',
|
||||
'User-Agent': 'ua',
|
||||
Accept: 'text/html'
|
||||
}
|
||||
stripClientHints(headers)
|
||||
expect(Object.keys(headers).some((k) => k.toLowerCase().startsWith('sec-ch-ua'))).toBe(false)
|
||||
expect(headers['User-Agent']).toBe('ua')
|
||||
expect(headers.Accept).toBe('text/html')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setUserAgentHeader', () => {
|
||||
it('overwrites an existing user-agent header in place, preserving its casing', () => {
|
||||
const headers: Record<string, string> = { 'user-agent': 'old' }
|
||||
setUserAgentHeader(headers, 'new')
|
||||
expect(headers['user-agent']).toBe('new')
|
||||
expect(Object.keys(headers)).toEqual(['user-agent'])
|
||||
})
|
||||
|
||||
it('adds a canonical header when none exists', () => {
|
||||
const headers: Record<string, string> = {}
|
||||
setUserAgentHeader(headers, 'new')
|
||||
expect(headers['User-Agent']).toBe('new')
|
||||
})
|
||||
})
|
||||
@@ -1,317 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { screenGetCursorScreenPointMock } = vi.hoisted(() => ({
|
||||
screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 }))
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
screen: { getCursorScreenPoint: screenGetCursorScreenPointMock }
|
||||
}))
|
||||
|
||||
import { setupGuestContextMenu } from './browser-guest-context-menu'
|
||||
|
||||
describe('setupGuestContextMenu', () => {
|
||||
const browserTabId = 'tab-1'
|
||||
let rendererSendMock: ReturnType<typeof vi.fn>
|
||||
let guestOnMock: ReturnType<typeof vi.fn>
|
||||
let guestOffMock: ReturnType<typeof vi.fn>
|
||||
|
||||
function makeGuest(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
getURL: vi.fn(() => 'https://example.com'),
|
||||
canGoBack: vi.fn(() => true),
|
||||
canGoForward: vi.fn(() => false),
|
||||
navigationHistory: {
|
||||
canGoBack: vi.fn(() => true),
|
||||
canGoForward: vi.fn(() => false)
|
||||
},
|
||||
on: guestOnMock,
|
||||
off: guestOffMock,
|
||||
...overrides
|
||||
} as unknown as Electron.WebContents
|
||||
}
|
||||
|
||||
function makeRenderer() {
|
||||
return { send: rendererSendMock } as unknown as Electron.WebContents
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
rendererSendMock = vi.fn()
|
||||
guestOnMock = vi.fn()
|
||||
guestOffMock = vi.fn()
|
||||
screenGetCursorScreenPointMock.mockReturnValue({ x: 0, y: 0 })
|
||||
})
|
||||
|
||||
function triggerContextMenu(
|
||||
_guest: Electron.WebContents,
|
||||
params: Partial<Electron.ContextMenuParams>
|
||||
) {
|
||||
const handler = guestOnMock.mock.calls.find((call) => call[0] === 'context-menu')?.[1] as
|
||||
| ((event: unknown, params: Electron.ContextMenuParams) => void)
|
||||
| undefined
|
||||
|
||||
expect(handler).toBeTypeOf('function')
|
||||
handler!({}, { x: 0, y: 0, linkURL: '', ...params } as Electron.ContextMenuParams)
|
||||
}
|
||||
|
||||
it('passes through guest viewport coordinates (params.x/y) to the renderer', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 150, y: 275 })
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:context-menu-requested',
|
||||
expect.objectContaining({ x: 150, y: 275 })
|
||||
)
|
||||
})
|
||||
|
||||
it('includes navigation state and page URL alongside coordinates', () => {
|
||||
screenGetCursorScreenPointMock.mockReturnValue({ x: 500, y: 375 })
|
||||
const guest = makeGuest({
|
||||
getURL: vi.fn(() => 'https://test.dev/page'),
|
||||
navigationHistory: {
|
||||
canGoBack: vi.fn(() => true),
|
||||
canGoForward: vi.fn(() => true)
|
||||
}
|
||||
})
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 50, y: 75, linkURL: 'https://test.dev/link' })
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('browser:context-menu-requested', {
|
||||
browserPageId: browserTabId,
|
||||
x: 50,
|
||||
y: 75,
|
||||
screenX: 500,
|
||||
screenY: 375,
|
||||
pageUrl: 'https://test.dev/page',
|
||||
linkUrl: 'https://test.dev/link',
|
||||
selectionText: '',
|
||||
canGoBack: true,
|
||||
canGoForward: true,
|
||||
editable: null,
|
||||
image: null
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards the native selection text so the renderer can offer Copy', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 10, y: 20, selectionText: 'copied selection' })
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:context-menu-requested',
|
||||
expect.objectContaining({ selectionText: 'copied selection' })
|
||||
)
|
||||
})
|
||||
|
||||
it('reads navigation state from navigationHistory', () => {
|
||||
const deprecatedCanGoBack = vi.fn(() => false)
|
||||
const deprecatedCanGoForward = vi.fn(() => false)
|
||||
const guest = makeGuest({
|
||||
canGoBack: deprecatedCanGoBack,
|
||||
canGoForward: deprecatedCanGoForward,
|
||||
navigationHistory: {
|
||||
canGoBack: vi.fn(() => true),
|
||||
canGoForward: vi.fn(() => true)
|
||||
}
|
||||
})
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 50, y: 75 })
|
||||
|
||||
expect(deprecatedCanGoBack).not.toHaveBeenCalled()
|
||||
expect(deprecatedCanGoForward).not.toHaveBeenCalled()
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:context-menu-requested',
|
||||
expect.objectContaining({ canGoBack: true, canGoForward: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('describes the editable field and image under the cursor', () => {
|
||||
const renderer = makeRenderer()
|
||||
setupGuestContextMenu({ browserTabId, guest: makeGuest(), resolveRenderer: () => renderer })
|
||||
|
||||
triggerContextMenu(makeGuest(), {
|
||||
isEditable: true,
|
||||
editFlags: {
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
canCut: true,
|
||||
canCopy: true,
|
||||
canPaste: false,
|
||||
canSelectAll: true,
|
||||
canDelete: true,
|
||||
canEditRichly: false
|
||||
},
|
||||
misspelledWord: 'quikc',
|
||||
dictionarySuggestions: ['quick', 'quirk', 'quack', 'quiche', 'quince', 'quinoa'],
|
||||
mediaType: 'image',
|
||||
srcURL: 'https://cdn.test/a.png',
|
||||
hasImageContents: true
|
||||
})
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:context-menu-requested',
|
||||
expect.objectContaining({
|
||||
editable: {
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
canCut: true,
|
||||
canCopy: true,
|
||||
canPaste: false,
|
||||
canSelectAll: true,
|
||||
spelling: {
|
||||
suggestions: ['quick', 'quirk', 'quack', 'quiche', 'quince'],
|
||||
misspelledWord: 'quikc'
|
||||
}
|
||||
},
|
||||
image: { url: 'https://cdn.test/a.png', canCopy: true }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does not send when renderer is unavailable', () => {
|
||||
const guest = makeGuest()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => null
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 100, y: 200 })
|
||||
|
||||
expect(rendererSendMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cleans up context-menu listener on teardown', () => {
|
||||
const guest = makeGuest()
|
||||
|
||||
const cleanup = setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
cleanup()
|
||||
|
||||
expect(guestOffMock).toHaveBeenCalledWith('context-menu', expect.any(Function))
|
||||
})
|
||||
|
||||
describe('dismiss handler', () => {
|
||||
function triggerMouseEvent(button: string, type: string = 'mouseDown') {
|
||||
const beforeMouseHandler = guestOnMock.mock.calls.find(
|
||||
(call) => call[0] === 'before-mouse-event'
|
||||
)?.[1] as ((event: unknown, mouse: { type: string; button: string }) => void) | undefined
|
||||
|
||||
expect(beforeMouseHandler).toBeTypeOf('function')
|
||||
beforeMouseHandler!({}, { type, button })
|
||||
}
|
||||
|
||||
it('dismisses context menu on left-click', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 100, y: 200 })
|
||||
rendererSendMock.mockClear()
|
||||
|
||||
triggerMouseEvent('left')
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('browser:context-menu-dismissed', {
|
||||
browserPageId: browserTabId
|
||||
})
|
||||
})
|
||||
|
||||
it('does not dismiss context menu on right-click', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 100, y: 200 })
|
||||
rendererSendMock.mockClear()
|
||||
|
||||
triggerMouseEvent('right')
|
||||
|
||||
expect(rendererSendMock).not.toHaveBeenCalledWith(
|
||||
'browser:context-menu-dismissed',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('dismisses context menu on middle-click', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 100, y: 200 })
|
||||
rendererSendMock.mockClear()
|
||||
|
||||
triggerMouseEvent('middle')
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('browser:context-menu-dismissed', {
|
||||
browserPageId: browserTabId
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores non-mouseDown events', () => {
|
||||
const guest = makeGuest()
|
||||
const renderer = makeRenderer()
|
||||
|
||||
setupGuestContextMenu({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: () => renderer
|
||||
})
|
||||
|
||||
triggerContextMenu(guest, { x: 100, y: 200 })
|
||||
rendererSendMock.mockClear()
|
||||
|
||||
triggerMouseEvent('left', 'mouseMove')
|
||||
|
||||
expect(rendererSendMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,14 +9,10 @@ vi.mock('electron', () => ({
|
||||
webContents: { fromId: vi.fn() }
|
||||
}))
|
||||
|
||||
import {
|
||||
resolveGuestMouseWheelZoomDirection,
|
||||
setupGuestMouseWheelZoomForwarding
|
||||
} from './browser-guest-wheel-zoom'
|
||||
import { setupGuestMouseWheelZoomForwarding } from './browser-guest-wheel-zoom'
|
||||
import { setupGuestShortcutForwarding } from './browser-guest-shortcut-forwarding'
|
||||
|
||||
const zoomTo = (direction: 'in' | 'out' | 'reset') => ({ browserPageId: 'tab-1', direction })
|
||||
const historyTo = (direction: 'back' | 'forward') => ({ browserPageId: 'tab-1', direction })
|
||||
|
||||
describe('guest mouse wheel browser zoom', () => {
|
||||
const browserTabId = 'tab-1'
|
||||
@@ -64,37 +60,6 @@ describe('guest mouse wheel browser zoom', () => {
|
||||
guestOffMock = vi.fn()
|
||||
})
|
||||
|
||||
it('resolves ctrl wheel direction from guest mouse input', () => {
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: -120 }), 'win32')).toBe('in')
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: 120 }), 'linux')).toBe('out')
|
||||
})
|
||||
|
||||
it('allows command wheel only on macOS', () => {
|
||||
const commandWheel = mouseWheel({ modifiers: ['cmd'], deltaY: -120 })
|
||||
|
||||
expect(resolveGuestMouseWheelZoomDirection(commandWheel, 'darwin')).toBe('in')
|
||||
expect(resolveGuestMouseWheelZoomDirection(commandWheel, 'win32')).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores non-zoom wheel input', () => {
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: [], deltaY: -120 }), 'linux')
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: ['ctrl', 'alt'] }), 'linux')
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: ['ctrl', 'shift'] }), 'linux')
|
||||
).toBeNull()
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: 0 }), 'linux')).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(
|
||||
{ type: 'mouseMove', x: 0, y: 0, modifiers: ['ctrl'] },
|
||||
'linux'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('forwards ctrl wheel to browser page zoom and consumes the guest wheel event', () => {
|
||||
setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
@@ -123,18 +88,6 @@ describe('guest mouse wheel browser zoom', () => {
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cleans up the mouse wheel listener on teardown', () => {
|
||||
const cleanup = setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
cleanup()
|
||||
|
||||
expect(guestOffMock).toHaveBeenCalledWith('before-mouse-event', expect.any(Function))
|
||||
})
|
||||
})
|
||||
|
||||
describe('setupGuestShortcutForwarding', () => {
|
||||
@@ -250,151 +203,6 @@ describe('setupGuestShortcutForwarding', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('forwards browser page zoom shortcuts from focused guest pages', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
const zoomInPreventDefault = triggerBeforeInput({ code: 'Equal', key: '=' })
|
||||
const shiftedPlusPreventDefault = triggerBeforeInput({
|
||||
code: 'Equal',
|
||||
key: '+',
|
||||
shift: true
|
||||
})
|
||||
const zoomOutPreventDefault = triggerBeforeInput({ code: 'Minus', key: '-' })
|
||||
const numpadSubtractPreventDefault = triggerBeforeInput({ code: 'NumpadSubtract', key: '-' })
|
||||
const resetPreventDefault = triggerBeforeInput({ code: 'Digit0', key: '0' })
|
||||
const repeatPreventDefault = triggerBeforeInput({
|
||||
code: 'NumpadAdd',
|
||||
key: '+',
|
||||
isAutoRepeat: true
|
||||
})
|
||||
|
||||
expect(zoomInPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(shiftedPlusPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(zoomOutPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(numpadSubtractPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(resetPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(repeatPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:zoomBrowserPage', zoomTo('in'))
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:zoomBrowserPage', zoomTo('in'))
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:zoomBrowserPage', zoomTo('out'))
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:zoomBrowserPage', zoomTo('out'))
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:zoomBrowserPage', zoomTo('reset'))
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:zoomBrowserPage', zoomTo('in'))
|
||||
})
|
||||
|
||||
it('forwards browser history shortcuts from focused guest pages', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
const backInput =
|
||||
process.platform === 'darwin'
|
||||
? { code: 'BracketLeft', key: '[', meta: true, control: false, alt: false }
|
||||
: { code: 'ArrowLeft', key: 'ArrowLeft', meta: false, control: false, alt: true }
|
||||
const forwardInput =
|
||||
process.platform === 'darwin'
|
||||
? { code: 'BracketRight', key: ']', meta: true, control: false, alt: false }
|
||||
: { code: 'ArrowRight', key: 'ArrowRight', meta: false, control: false, alt: true }
|
||||
|
||||
const backPreventDefault = triggerBeforeInput(backInput)
|
||||
const forwardPreventDefault = triggerBeforeInput(forwardInput)
|
||||
|
||||
expect(backPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(forwardPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'ui:browserHistoryNavigate',
|
||||
historyTo('back')
|
||||
)
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'ui:browserHistoryNavigate',
|
||||
historyTo('forward')
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards browser Find with its registered page and workspace owner', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer(),
|
||||
resolveWorkspaceId: () => 'workspace-1'
|
||||
})
|
||||
|
||||
const preventDefault = triggerBeforeInput({ code: 'KeyF', key: 'f' })
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('ui:findInBrowserPage', {
|
||||
browserPageId: browserTabId,
|
||||
browserWorkspaceId: 'workspace-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards browser Find by page alone when no workspace owner is registered', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
const preventDefault = triggerBeforeInput({ code: 'KeyF', key: 'f' })
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('ui:findInBrowserPage', {
|
||||
browserPageId: browserTabId,
|
||||
browserWorkspaceId: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards quick-command menu shortcuts from focused guest pages', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer(),
|
||||
getKeybindings: () => ({
|
||||
'tab.openQuickCommandsMenu': ['Mod+Shift+Q']
|
||||
})
|
||||
})
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
const preventDefault = triggerBeforeInput({
|
||||
code: 'KeyQ',
|
||||
key: 'q',
|
||||
meta: isMac,
|
||||
control: !isMac,
|
||||
shift: true
|
||||
})
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('ui:toggleQuickCommandsMenu')
|
||||
})
|
||||
|
||||
it('forwards workspace delete shortcuts from focused guest pages', () => {
|
||||
setupGuestShortcutForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
const preventDefault = triggerBeforeInput({
|
||||
code: 'Backspace',
|
||||
key: 'Backspace',
|
||||
meta: isMac,
|
||||
control: !isMac,
|
||||
shift: true
|
||||
})
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
expect(rendererSendMock).toHaveBeenCalledWith('ui:deleteCurrentWorkspace')
|
||||
})
|
||||
|
||||
// Why: Orca claims these on every repeat but acts once, so a held chord's repeats never reach the page.
|
||||
it.each([
|
||||
['workspace delete', { code: 'Backspace', key: 'Backspace' }],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
@@ -28,17 +28,6 @@ describe('browser host client identity', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('persists the identity where the next launch reads it', () => {
|
||||
const directory = profileDirectory()
|
||||
|
||||
const identity = readOrCreateBrowserHostClientId(directory)
|
||||
|
||||
expect(JSON.parse(readFileSync(browserHostClientIdentityPath(directory), 'utf-8'))).toEqual({
|
||||
version: 1,
|
||||
browserHostClientId: identity
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['unparseable content', 'not json at all'],
|
||||
['a future store version', JSON.stringify({ version: 2, browserHostClientId: 'kept' })],
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
nextBrowserHostReconnectDelay,
|
||||
resolveBrowserHostReconnectDelay
|
||||
} from './browser-host-lease-reconnect-delay'
|
||||
|
||||
describe('browser host lease reconnect delay', () => {
|
||||
it('uses deterministic client-specific jitter within exponential and grace bounds', () => {
|
||||
const delays = Array.from({ length: 8 }, (_, attempt) =>
|
||||
nextBrowserHostReconnectDelay({
|
||||
baseDelayMs: 100,
|
||||
attempt,
|
||||
remainingMs: 15_000,
|
||||
browserHostClientId: 'host-a'
|
||||
})
|
||||
)
|
||||
|
||||
expect(delays).toEqual(
|
||||
delays.map((_, attempt) =>
|
||||
nextBrowserHostReconnectDelay({
|
||||
baseDelayMs: 100,
|
||||
attempt,
|
||||
remainingMs: 15_000,
|
||||
browserHostClientId: 'host-a'
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(50)
|
||||
expect(delays[0]).toBeLessThanOrEqual(100)
|
||||
expect(Math.max(...delays)).toBeLessThanOrEqual(2_000)
|
||||
expect(
|
||||
nextBrowserHostReconnectDelay({
|
||||
baseDelayMs: 100,
|
||||
attempt: 8,
|
||||
remainingMs: 37,
|
||||
browserHostClientId: 'host-b'
|
||||
})
|
||||
).toBe(37)
|
||||
})
|
||||
|
||||
it('rejects invalid timer delays', () => {
|
||||
expect(resolveBrowserHostReconnectDelay(undefined)).toBe(100)
|
||||
expect(() => resolveBrowserHostReconnectDelay(0)).toThrow(
|
||||
'Browser host reconnect delay is invalid'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
BROWSER_IDENTITY_MODE_FILE,
|
||||
BROWSER_IDENTITY_MODE_VERSION,
|
||||
readBrowserIdentityModeRecord
|
||||
} from './browser-identity-mode-record'
|
||||
|
||||
function makeUserData(): string {
|
||||
return mkdtempSync(join(tmpdir(), 'orca-browser-identity-'))
|
||||
}
|
||||
|
||||
function writeRecord(userDataPath: string, value: unknown): void {
|
||||
writeFileSync(join(userDataPath, BROWSER_IDENTITY_MODE_FILE), JSON.stringify(value), 'utf8')
|
||||
}
|
||||
|
||||
describe('readBrowserIdentityModeRecord', () => {
|
||||
it('distinguishes missing data as implicit clean', () => {
|
||||
expect(readBrowserIdentityModeRecord(makeUserData())).toEqual({
|
||||
state: 'missing',
|
||||
appliedMode: 'clean',
|
||||
configuredMode: 'clean',
|
||||
explicitSelection: false,
|
||||
migrationNoticePending: false
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a valid configured identity', () => {
|
||||
const userDataPath = makeUserData()
|
||||
writeRecord(userDataPath, {
|
||||
version: BROWSER_IDENTITY_MODE_VERSION,
|
||||
mode: 'native',
|
||||
explicitSelection: true,
|
||||
migrationNoticePending: true
|
||||
})
|
||||
|
||||
expect(readBrowserIdentityModeRecord(userDataPath)).toEqual({
|
||||
state: 'valid',
|
||||
appliedMode: 'native',
|
||||
configuredMode: 'native',
|
||||
explicitSelection: true,
|
||||
migrationNoticePending: true
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to clean without inventing a configured mode for corrupt data', () => {
|
||||
const userDataPath = makeUserData()
|
||||
writeFileSync(join(userDataPath, BROWSER_IDENTITY_MODE_FILE), '{not json', 'utf8')
|
||||
|
||||
expect(readBrowserIdentityModeRecord(userDataPath)).toEqual({
|
||||
state: 'corrupt',
|
||||
appliedMode: 'clean',
|
||||
configuredMode: null,
|
||||
explicitSelection: null,
|
||||
migrationNoticePending: null
|
||||
})
|
||||
})
|
||||
|
||||
it('distinguishes a future record from corrupt data', () => {
|
||||
const userDataPath = makeUserData()
|
||||
writeRecord(userDataPath, {
|
||||
version: BROWSER_IDENTITY_MODE_VERSION + 1,
|
||||
mode: 'native',
|
||||
explicitSelection: true,
|
||||
migrationNoticePending: false
|
||||
})
|
||||
|
||||
expect(readBrowserIdentityModeRecord(userDataPath)).toEqual({
|
||||
state: 'future',
|
||||
appliedMode: 'clean',
|
||||
configuredMode: null,
|
||||
explicitSelection: null,
|
||||
migrationNoticePending: null
|
||||
})
|
||||
})
|
||||
|
||||
it('distinguishes an unreadable record from missing data', () => {
|
||||
const userDataPath = makeUserData()
|
||||
mkdirSync(join(userDataPath, BROWSER_IDENTITY_MODE_FILE))
|
||||
|
||||
expect(readBrowserIdentityModeRecord(userDataPath)).toEqual({
|
||||
state: 'unreadable',
|
||||
appliedMode: 'clean',
|
||||
configuredMode: null,
|
||||
explicitSelection: null,
|
||||
migrationNoticePending: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -178,41 +178,6 @@ describe('production link bridge, store and mounted guest', () => {
|
||||
expect(receipts.some((receipt) => receipt.path === '/removed-page')).toBe(false)
|
||||
})
|
||||
|
||||
// These assertions capture unsafe baseline behavior pending claim handoff repair.
|
||||
it.each(['remove-tab', 'profile-switch', 'delete-profile'])(
|
||||
'records unsafe stale-source baseline through the real mount: %s',
|
||||
async (transition) => {
|
||||
rendererErrors.length = 0
|
||||
await page.reload()
|
||||
await ready()
|
||||
const path = `/baseline-${transition}`
|
||||
await open(false, path, true)
|
||||
await page.evaluate(`window.linkProbe.mutate(${JSON.stringify(transition)})`)
|
||||
await page.evaluate('window.probeApi.fixture.release()')
|
||||
await page.evaluate('window.linkProbe.mountTabs()')
|
||||
await expect
|
||||
.poll(() => receipts.some((receipt) => receipt.path === path), { timeout: 10_000 })
|
||||
.toBe(true)
|
||||
const state = await page.evaluate<LinkSnapshot>('window.linkProbe.snapshot()')
|
||||
const replacement = state.tabs.find((tab) => tab.id !== state.sourceWorkspaceId)
|
||||
expect(replacement).toBeDefined()
|
||||
const expectedProfile = transition === 'delete-profile' ? 'isolated' : 'other'
|
||||
expect(receipts.filter((receipt) => receipt.path === path)).toEqual([
|
||||
{ path, cookie: `synthetic_profile=${expectedProfile}` }
|
||||
])
|
||||
await expect
|
||||
.poll(() => page.evaluate('window.probeApi.fixture.guests()'), { timeout: 10_000 })
|
||||
.toEqual([
|
||||
expect.objectContaining({
|
||||
url: `${origin}${path}`,
|
||||
isolated: expectedProfile === 'isolated',
|
||||
other: expectedProfile === 'other'
|
||||
})
|
||||
])
|
||||
},
|
||||
25_000
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'rejects removed source tabs, profile switches and deleted profiles without a tab, mount, default fallback or destination request'
|
||||
)
|
||||
|
||||
@@ -97,28 +97,6 @@ describe('browserManager.setAnnotationViewportBridge', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('installs the bridge in an isolated world', async () => {
|
||||
const { guest } = makeGuest(4646)
|
||||
registerPage('tab-annotations', guest)
|
||||
|
||||
const ok = await browserManager.setAnnotationViewportBridge(
|
||||
'tab-annotations',
|
||||
BRIDGE_OPTIONS,
|
||||
resolveFromRegistry('tab-annotations')
|
||||
)
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(guest.executeJavaScriptInIsolatedWorld).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
[
|
||||
expect.objectContaining({
|
||||
code: expect.stringContaining('__orcaBrowserAnnotationViewportBridge')
|
||||
})
|
||||
],
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
// Navigation can replace the registered guest while its old WebContents remains alive.
|
||||
it('injects into the guest the page has when a queued op finally runs, not the one it was asked with', async () => {
|
||||
const { guest: firstGuest } = makeGuest(5101)
|
||||
|
||||
@@ -390,44 +390,6 @@ describe('browserManager', () => {
|
||||
})
|
||||
|
||||
// Direct navigation permits setUserAgent before request dispatch.
|
||||
it('still uses the WebContents UA write for navigations that are not redirects', () => {
|
||||
const guest = {
|
||||
id: 420,
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isCurrentlyAudible: vi.fn(() => false),
|
||||
send: vi.fn(),
|
||||
getType: vi.fn(() => 'webview'),
|
||||
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
||||
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
||||
on: guestOnMock,
|
||||
off: guestOffMock,
|
||||
openDevTools: guestOpenDevToolsMock,
|
||||
getURL: vi.fn(() => 'https://example.com/'),
|
||||
getUserAgent: vi.fn(() => guestBaseUserAgent),
|
||||
setUserAgent: vi.fn(),
|
||||
debugger: { isAttached: vi.fn(() => true), sendCommand: vi.fn(async () => undefined) },
|
||||
session: { getUserAgent: vi.fn(() => guestBaseUserAgent) }
|
||||
}
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: 'browser-direct-ua',
|
||||
webContentsId: guest.id,
|
||||
rendererWebContentsId
|
||||
})
|
||||
const didStartNavigation = guestOnMock.mock.calls.find(
|
||||
([event]) => event === 'did-start-navigation'
|
||||
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
|
||||
|
||||
didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true)
|
||||
|
||||
expect(guest.setUserAgent).toHaveBeenLastCalledWith(googleAuthUserAgent())
|
||||
expect(guest.debugger.sendCommand).not.toHaveBeenCalledWith(
|
||||
'Emulation.setUserAgentOverride',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
// Keep the process UA override until direct navigation can replace the stale Firefox base UA.
|
||||
it('never clears the override onto a stale Firefox WebContents UA and heals it on the next navigation', async () => {
|
||||
|
||||
@@ -223,34 +223,6 @@ describe('client-hosted downloads', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('reports the whole lifecycle of a plain client-hosted download to its page renderer', async () => {
|
||||
const { route, completed } = stubRoute()
|
||||
registerBrowserClientDownloadRouter('env-a', { route: () => ({ kind: 'remote', route }) })
|
||||
const item = createDownloadItem()
|
||||
|
||||
browserManager.handleGuestWillDownload({ guestWebContentsId: GUEST_WEB_CONTENTS_ID, item })
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:download-requested',
|
||||
expect.objectContaining({ browserPageId: BROWSER_PAGE_ID, status: 'downloading' })
|
||||
)
|
||||
|
||||
getDownloadItemEventHandler(item, 'once', 'done')?.({} as Electron.Event, 'completed')
|
||||
await completed
|
||||
|
||||
expect(rendererSendMock).toHaveBeenCalledWith(
|
||||
'browser:download-finished',
|
||||
expect.objectContaining({
|
||||
browserPageId: BROWSER_PAGE_ID,
|
||||
status: 'completed',
|
||||
remoteDestination: {
|
||||
workspaceRelativePath: '.orca/browser-downloads/report.csv',
|
||||
hostLabel: 'build-box'
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('cancels a client-hosted download that no composition owns', () => {
|
||||
registerBrowserClientDownloadRouter('env-a', { route: () => ({ kind: 'unowned' }) })
|
||||
const item = createDownloadItem()
|
||||
|
||||
@@ -72,14 +72,6 @@ describe('browser guest destruction ownership', () => {
|
||||
expectRetainedCount(0)
|
||||
})
|
||||
|
||||
it('leaves no dead-guest callbacks for the window-close unregisterAll path', () => {
|
||||
for (let index = 0; index < 1000; index++) {
|
||||
register(`window-${index}`).destroy()
|
||||
}
|
||||
manager.unregisterAll()
|
||||
expectRetainedCount(0)
|
||||
})
|
||||
|
||||
it('keeps explicit unregister before destruction idempotent', () => {
|
||||
const guest = register('explicit')
|
||||
manager.unregisterGuest('explicit')
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
projectHostSetupProjectionFromRepos,
|
||||
getProjectHostSetupsForProject,
|
||||
isGitHubBackedRepo,
|
||||
getProjectIdForProviderIdentity,
|
||||
isProjectRemoteIdentityPending
|
||||
} from './project-host-setup-projection'
|
||||
@@ -449,31 +448,6 @@ describe('project host setup projection', () => {
|
||||
).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('keeps same-named cross-host records separate when there is no shared repo identity', () => {
|
||||
const projection = projectHostSetupProjectionFromRepos([
|
||||
repo({
|
||||
id: 'local-sample-app',
|
||||
path: '/Users/alice/work/sample-app',
|
||||
displayName: 'sample-app'
|
||||
}),
|
||||
repo({
|
||||
id: 'ssh-sample-app',
|
||||
path: '/srv/unrelated/sample-app',
|
||||
displayName: 'sample-app',
|
||||
connectionId: 'staging server'
|
||||
}),
|
||||
repo({
|
||||
id: 'runtime-sample-app',
|
||||
path: '/workspace/sample-app',
|
||||
displayName: 'sample-app',
|
||||
executionHostId: 'runtime:preview'
|
||||
})
|
||||
])
|
||||
|
||||
// Display names cannot prove identity; merge only with normalized remote identity or explicit user linking.
|
||||
expect(projection.projects).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('does not collapse case-distinct remote paths for self-hosted git remotes', () => {
|
||||
const projection = projectHostSetupProjectionFromRepos([
|
||||
repo({
|
||||
@@ -537,47 +511,6 @@ describe('project host setup projection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGitHubBackedRepo', () => {
|
||||
it('is true when an explicit upstream owner/repo is present', () => {
|
||||
const target = repo({
|
||||
id: 'r',
|
||||
path: '/r',
|
||||
displayName: 'r',
|
||||
upstream: { owner: 'stablyai', repo: 'orca' }
|
||||
})
|
||||
expect(isGitHubBackedRepo(target)).toBe(true)
|
||||
})
|
||||
|
||||
it('is true when a GitHub-sourced avatar icon encodes the slug', () => {
|
||||
const target = repo({
|
||||
id: 'r',
|
||||
path: '/r',
|
||||
displayName: 'r',
|
||||
repoIcon: {
|
||||
type: 'image',
|
||||
src: 'https://github.com/stablyai.png?size=64',
|
||||
source: 'github',
|
||||
label: 'stablyai/orca'
|
||||
}
|
||||
})
|
||||
expect(isGitHubBackedRepo(target)).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for a non-GitHub icon and no upstream (GitLab/folder)', () => {
|
||||
const target = repo({
|
||||
id: 'r',
|
||||
path: '/r',
|
||||
displayName: 'r',
|
||||
repoIcon: { type: 'lucide', name: 'gitlab' }
|
||||
})
|
||||
expect(isGitHubBackedRepo(target)).toBe(false)
|
||||
})
|
||||
|
||||
it('is false for a plain local repo with no provider signal', () => {
|
||||
expect(isGitHubBackedRepo(repo({ id: 'r', path: '/r', displayName: 'r' }))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProjectIdForProviderIdentity', () => {
|
||||
it('uses the same normalized identity key as project projection', () => {
|
||||
expect(
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { DEFAULT_TERMINAL_THEME_DARK } from './terminal-theme-selection'
|
||||
import { TERMINAL_THEME_CATALOG } from './terminal-themes'
|
||||
import {
|
||||
ORCA_DEFAULT_COLOR_QUERY_REPLY_COLORS,
|
||||
normalizeColorQueryReplyColors,
|
||||
resolvePtyOwnerColorQueryColors
|
||||
} from './pty-owner-color-query-colors'
|
||||
|
||||
it("falls back to the colours of Orca's default dark terminal theme", () => {
|
||||
const theme = TERMINAL_THEME_CATALOG[DEFAULT_TERMINAL_THEME_DARK]
|
||||
expect(ORCA_DEFAULT_COLOR_QUERY_REPLY_COLORS).toEqual({
|
||||
foreground: theme?.foreground,
|
||||
background: theme?.background
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers host colours, then spawn colours, then the default, skipping unusable pairs', () => {
|
||||
const host = { foreground: '#000000', background: '#111111' }
|
||||
const spawn = { foreground: '#222222', background: '#333333' }
|
||||
|
||||
@@ -37,16 +37,6 @@ describe('PTY owner colour replies for the terminal life', () => {
|
||||
expect(visible()).toBe('\x1b]11;#123456\x07\x1b]111\x07')
|
||||
})
|
||||
|
||||
it('answers with the viewer colours the host holds now, not the colours sent at spawn', () => {
|
||||
// The host's viewer may have changed theme since this pane was created.
|
||||
const spawnedLight = { foreground: '#2e3434', background: '#ffffff' }
|
||||
const { ingress, writes } = createHarness({ colors: spawnedLight, deadlineMs: 5_000 })
|
||||
|
||||
ingress.accept(QUERY)
|
||||
|
||||
expect(writes).toEqual([HOST_BACKGROUND_REPLY])
|
||||
})
|
||||
|
||||
it('answers a query torn right after its ESC once the Kitty window has closed', () => {
|
||||
const { ingress, writes, visible } = createHarness({
|
||||
colors: HOST,
|
||||
|
||||
@@ -46,14 +46,6 @@ describe('live query replies', () => {
|
||||
expect(needsCookedEchoSafeQueryReply(mode2031SequenceFor('light'))).toBe(true)
|
||||
})
|
||||
|
||||
it('writes an echo-risk reply in the calling turn', () => {
|
||||
const { ingress, writes } = harness()
|
||||
expect(ingress.answerLiveQueryReply(OSC_COLOR_REPLY)).toBe(true)
|
||||
// No timer advance: the whole point is that there is nothing to wait for.
|
||||
expect(writes).toEqual([OSC_COLOR_REPLY])
|
||||
ingress.drainAndClose()
|
||||
})
|
||||
|
||||
it('answers repeated identical replies without collapsing them', () => {
|
||||
const { ingress, writes } = harness()
|
||||
expect(ingress.answerLiveQueryReply(COLOR_SCHEME_REPLY)).toBe(true)
|
||||
|
||||
@@ -79,16 +79,6 @@ describe('PtyStartupIngress', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('suppresses the first echo immediately and keeps a later exact collision', () => {
|
||||
const { ingress, emissions } = createHarness({ projection: true })
|
||||
ingress.accept('\x1b]10;?\x07')
|
||||
const projected = ']10;rgb:2e2e/3434/3434\\'
|
||||
ingress.accept(projected)
|
||||
ingress.accept(projected)
|
||||
ingress.drainAndClose()
|
||||
expect(visible(emissions)).toBe(projected)
|
||||
})
|
||||
|
||||
it('matches each echo across every split without skipping an earlier FIFO candidate', () => {
|
||||
const foregroundEcho = ']10;rgb:2e2e/3434/3434\\'
|
||||
const backgroundEcho = ']11;rgb:ffff/ffff/ffff\\'
|
||||
@@ -751,32 +741,4 @@ describe('PtyStartupIngress', () => {
|
||||
expect(ingress.acceptedRawSequence).toBe(closedAt)
|
||||
expect(visible(emissions)).toBe(']10;rgb:2e2e/')
|
||||
})
|
||||
|
||||
it('suppresses the readline echo, which no reading of the ECHO bit could predict', () => {
|
||||
const { ingress, writes, emissions } = createHarness()
|
||||
ingress.accept('\x1b]10;?\x07')
|
||||
expect(writes).toEqual([FOREGROUND_REPLY])
|
||||
// Live bash PTYs echo via readline even with raw mode/ECHO-clear; kernel ECHO cannot predict software echoes.
|
||||
ingress.accept(POSIX_COOKED_ECHOES[1]?.(FOREGROUND_REPLY) ?? '')
|
||||
ingress.drainAndClose()
|
||||
expect(visible(emissions)).toBe('')
|
||||
})
|
||||
|
||||
it('suppresses the kernel caret echo', () => {
|
||||
const { ingress, writes, emissions } = createHarness()
|
||||
ingress.accept('\x1b]10;?\x07')
|
||||
expect(writes).toEqual([FOREGROUND_REPLY])
|
||||
// Always arm the default ECHOCTL projection; no probe verdict can safely retire it.
|
||||
ingress.accept(POSIX_COOKED_ECHOES[0]?.(FOREGROUND_REPLY) ?? '')
|
||||
ingress.drainAndClose()
|
||||
expect(visible(emissions)).toBe('')
|
||||
})
|
||||
|
||||
it('writes the reply in the accepting turn, with no probe and no wait', () => {
|
||||
const { ingress, writes } = createHarness()
|
||||
ingress.accept('\u001b]10;?\u0007')
|
||||
// No deferral: only output projections remove echo, while delayed writes reorder replies (#15559).
|
||||
expect(writes).toEqual(['\u001b]10;rgb:2e2e/3434/3434\u001b\\'])
|
||||
ingress.drainAndClose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,26 +40,6 @@ describe('replyEchoProjections on a POSIX pty', () => {
|
||||
expect(locateEcho(replyEchoProjections(reply, 'posix-pty'), reply).kind).toBe('complete')
|
||||
})
|
||||
|
||||
it('caret-encodes every control, not just ESC', () => {
|
||||
const [kernel] = replyEchoProjections(OSC11_BEL, 'posix-pty')
|
||||
expect(kernel?.needle).toBe('^[]11;rgb:2e2e/3434/3434^G')
|
||||
expect(kernel?.needle).not.toContain('\x07')
|
||||
})
|
||||
|
||||
// ECHOCTL leaves TAB/LF/CR literal and maps DEL→^? (vis(3)); pin future grammar behavior even though current replies lack them.
|
||||
it.each([
|
||||
{ name: 'TAB stays literal', input: '\t', encoded: '\t' },
|
||||
{ name: 'LF stays literal', input: '\n', encoded: '\n' },
|
||||
{ name: 'CR stays literal', input: '\r', encoded: '\r' },
|
||||
{ name: 'NUL carets to ^@', input: '\x00', encoded: '^@' },
|
||||
{ name: 'BEL carets to ^G', input: '\x07', encoded: '^G' },
|
||||
{ name: 'ESC carets to ^[', input: '\x1b', encoded: '^[' },
|
||||
{ name: 'DEL carets to ^?', input: '\x7f', encoded: '^?' }
|
||||
])('$name under ECHOCTL', ({ input, encoded }) => {
|
||||
const [kernel] = replyEchoProjections(`\x1b[?9${input}n`, 'posix-pty')
|
||||
expect(kernel?.needle).toBe(`^[[?9${encoded}n`)
|
||||
})
|
||||
|
||||
// Guard the private-DSR grammar locally: widening a caller must not arm DA1’s shared-prefix BEL needle.
|
||||
it.each(['\x1b[?1;2c', '\x1b[?0u', '\x1b[?2026;2$y', '\x1b[?12;5R'])(
|
||||
'projects no readline needle for %j, which is not a private DSR',
|
||||
@@ -82,13 +62,6 @@ describe('replyEchoProjections on a POSIX pty', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('projects readline for a private DSR, which carries no OSC introducer', () => {
|
||||
const needles = replyEchoProjections(DSR_997, 'posix-pty').map(
|
||||
(projection) => projection.needle
|
||||
)
|
||||
expect(needles).toContain('\x07997;1n')
|
||||
})
|
||||
|
||||
// Never hold ESC-led partials: a bare ESC could belong to a torn query and would escape unanswered when the hold expires.
|
||||
it('holds a partial only for needles that do not start with ESC', () => {
|
||||
for (const { reply } of LIVE_ECHOES) {
|
||||
|
||||
@@ -22,11 +22,6 @@ describe('resolvePublishedPaneAgentIdentity', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the launch record when an unambiguous title names a different agent', () => {
|
||||
// Unlike agreeing/absent title cases, this detects the title-first mutant: explicit Claude branding cannot override a Codex launch.
|
||||
expect(resolve({ launchAgent: 'codex', title: '✳ Claude Code' })).toBe('codex')
|
||||
})
|
||||
|
||||
it('prefers the live foreground process to the launch record', () => {
|
||||
expect(resolve({ launchAgent: 'claude', foregroundAgent: 'codex' })).toBe('codex')
|
||||
})
|
||||
|
||||
@@ -33,14 +33,6 @@ describe('buildPullRequestFieldsPrompt', () => {
|
||||
expect(prompt).toContain('Use conventional PR titles.')
|
||||
})
|
||||
|
||||
it('requires ELI5 problem and solution sections before implementation details', () => {
|
||||
const prompt = buildPullRequestFieldsPrompt(context, '')
|
||||
|
||||
expect(prompt).toContain('start with `## Problem`, then `## Solution`')
|
||||
expect(prompt).toContain('simple ELI5 language before details')
|
||||
expect(prompt).toContain('Reuse equivalent existing sections instead of duplicating them')
|
||||
})
|
||||
|
||||
it('includes GitHub issue details and complete or partial reference guidance', () => {
|
||||
const prompt = buildPullRequestFieldsPrompt(
|
||||
{
|
||||
@@ -110,19 +102,6 @@ describe('buildPullRequestFieldsPrompt', () => {
|
||||
expect(prompt).toContain('`Fixes AB#99` only for a complete fix')
|
||||
expect(prompt).toContain('use `AB#99`')
|
||||
})
|
||||
|
||||
it('tells the agent to preserve existing review templates', () => {
|
||||
const prompt = buildPullRequestFieldsPrompt(
|
||||
{
|
||||
...context,
|
||||
currentBody: '## Summary\n\n## Testing\n\n- [ ] Required checks'
|
||||
},
|
||||
''
|
||||
)
|
||||
|
||||
expect(prompt).toContain('Retain every heading, required section, and checklist')
|
||||
expect(prompt).toContain('Leave genuinely unknown template items as TODO or unchecked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseGeneratedPullRequestFields', () => {
|
||||
@@ -140,28 +119,6 @@ describe('parseGeneratedPullRequestFields', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('parses CRLF fenced JSON output without full-string fence matching', () => {
|
||||
const matchSpy = vi.spyOn(String.prototype, 'match')
|
||||
const replaceSpy = vi.spyOn(String.prototype, 'replace')
|
||||
const fields = parseGeneratedPullRequestFields(
|
||||
'```JSON\r\n{"base":"main","title":"fix: add details.","body":"Summary","draft":true}\r\n```',
|
||||
context
|
||||
)
|
||||
|
||||
expect(fields.title).toBe('fix: add details')
|
||||
const usedFenceMatch = matchSpy.mock.calls.some(
|
||||
([pattern]) =>
|
||||
pattern instanceof RegExp &&
|
||||
pattern.source.startsWith('^```') &&
|
||||
pattern.source.includes('[\\s\\S]')
|
||||
)
|
||||
const usedCrlfReplace = replaceSpy.mock.calls.some(
|
||||
([pattern]) => pattern instanceof RegExp && pattern.source === '\\r\\n' && pattern.global
|
||||
)
|
||||
expect(usedFenceMatch).toBe(false)
|
||||
expect(usedCrlfReplace).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back for missing optional values', () => {
|
||||
const fields = parseGeneratedPullRequestFields('{"title":""}', context)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
buildGitLsFilesArgsForQuickOpen,
|
||||
buildHiddenDirExcludeGlobs,
|
||||
buildRgArgsForQuickOpen,
|
||||
HIDDEN_DIR_BLOCKLIST,
|
||||
normalizeQuickOpenRgLine,
|
||||
shouldExcludeQuickOpenRelPath,
|
||||
shouldIncludeQuickOpenPath
|
||||
@@ -38,14 +37,6 @@ describe('shouldIncludeQuickOpenPath', () => {
|
||||
expect(shouldIncludeQuickOpenPath('nested/.local/share/app/state.db')).toBe(false)
|
||||
expect(shouldIncludeQuickOpenPath('.local/bin/tool')).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT blocklist user-authored dirs like .config, .ssh, .github', () => {
|
||||
expect(HIDDEN_DIR_BLOCKLIST.has('.config')).toBe(false)
|
||||
expect(HIDDEN_DIR_BLOCKLIST.has('.ssh')).toBe(false)
|
||||
expect(HIDDEN_DIR_BLOCKLIST.has('.github')).toBe(false)
|
||||
expect(HIDDEN_DIR_BLOCKLIST.has('.devcontainer')).toBe(false)
|
||||
expect(HIDDEN_DIR_BLOCKLIST.has('.local')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildExcludePathPrefixes', () => {
|
||||
@@ -91,40 +82,9 @@ describe('buildExcludePathPrefixes', () => {
|
||||
it('strips trailing slashes', () => {
|
||||
expect(buildExcludePathPrefixes('/r', ['/r/a/', '/r/b///'])).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('keeps valid child prefixes whose segment starts with dotdot characters', () => {
|
||||
expect(buildExcludePathPrefixes('/home/u/repo', ['/home/u/repo/..fixtures'])).toEqual([
|
||||
'..fixtures'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldExcludeQuickOpenRelPath', () => {
|
||||
it('matches the original filter across boundary and Unicode path combinations', () => {
|
||||
const paths = [
|
||||
'',
|
||||
'/',
|
||||
'a',
|
||||
'a/',
|
||||
'a//',
|
||||
'ab',
|
||||
'a/b',
|
||||
'a\\b',
|
||||
'A/b',
|
||||
'界/😀',
|
||||
'界/😀x',
|
||||
'a[1]/x',
|
||||
'a./x'
|
||||
]
|
||||
for (const prefix of paths) {
|
||||
for (const relPath of paths) {
|
||||
const expected =
|
||||
relPath === prefix || (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`))
|
||||
expect(shouldExcludeQuickOpenRelPath(relPath, [prefix])).toBe(expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves normalized Windows and UNC exclusion boundaries', () => {
|
||||
for (const [root, excluded] of [
|
||||
['C:\\Repo', 'C:\\Repo\\trees\\one'],
|
||||
@@ -327,14 +287,6 @@ describe('buildGitLsFilesArgsForQuickOpen', () => {
|
||||
expect(ignoredPass).not.toContain(':(glob)**/.env*')
|
||||
})
|
||||
|
||||
it('collapses untracked directories in both passes without generated pathspec churn', () => {
|
||||
const { primary, ignoredPass } = buildGitLsFilesArgsForQuickOpen()
|
||||
expect(primary).toContain('--directory')
|
||||
expect(ignoredPass).toContain('--directory')
|
||||
expect(ignoredPass).toContain('--no-empty-directory')
|
||||
expect([...primary, ...ignoredPass]).not.toContain(':(exclude,glob)**/node_modules/**')
|
||||
})
|
||||
|
||||
it('exclude prefixes prepend positive "." pathspec', () => {
|
||||
const { primary, ignoredPass } = buildGitLsFilesArgsForQuickOpen(['packages/app'])
|
||||
const dashDashIdx = primary.indexOf('--')
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
limitQuickOpenFilesBySerializedBytes,
|
||||
limitQuickOpenSearchReplyBySerializedBytes,
|
||||
serializedQuickOpenPathBytes
|
||||
limitQuickOpenSearchReplyBySerializedBytes
|
||||
} from './quick-open-transport-budget'
|
||||
|
||||
describe('Quick Open transport byte budget', () => {
|
||||
it('matches JSON.stringify byte accounting, including escaped paths', () => {
|
||||
const path = 'src/line\nquote".ts'
|
||||
expect(serializedQuickOpenPathBytes(path)).toBe(Buffer.byteLength(JSON.stringify(path), 'utf8'))
|
||||
})
|
||||
|
||||
it('returns the largest prefix that fits as a JSON array', () => {
|
||||
const files = ['src/a.ts', 'data/ignored\nfile.bin', 'src/b.ts']
|
||||
const budget = Buffer.byteLength(JSON.stringify(files.slice(0, 2)), 'utf8')
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { decodeBase64Prefix, exceedsRasterImagePreviewLimits } from './raster-image-base64-preview'
|
||||
import type * as RasterImageDimensionsModule from './raster-image-dimensions'
|
||||
import { readRasterImageDimensions } from './raster-image-dimensions'
|
||||
import {
|
||||
isKnownRasterImageMimeType,
|
||||
isRasterImagePreviewDimensions,
|
||||
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
|
||||
} from './raster-image-preview-limits'
|
||||
|
||||
/** Frozen full-payload decode/dimension baseline for comparison with the prefix probe. */
|
||||
function unprobedExceeds(content: string, mimeType: string | undefined): boolean {
|
||||
if (!isKnownRasterImageMimeType(mimeType)) {
|
||||
return false
|
||||
}
|
||||
const prefix = referenceDecodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
|
||||
if (!prefix) {
|
||||
return false
|
||||
}
|
||||
const dimensions = readRasterImageDimensions(prefix)
|
||||
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
|
||||
}
|
||||
import { RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES } from './raster-image-preview-limits'
|
||||
|
||||
// Observe decoded bytes handed to dimension reading; output equivalence alone cannot detect wasted full-payload decoding.
|
||||
const { dimensionReadLengths } = vi.hoisted(() => ({ dimensionReadLengths: [] as number[] }))
|
||||
@@ -35,123 +17,6 @@ vi.mock('./raster-image-dimensions', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ── Reference decoder: the pre-change implementation, verbatim ──────────────────────────────────
|
||||
const BASE64_PADDING = -2
|
||||
const INVALID_BASE64 = -1
|
||||
|
||||
function base64Value(code: number): number {
|
||||
if (code >= 65 && code <= 90) {
|
||||
return code - 65
|
||||
}
|
||||
if (code >= 97 && code <= 122) {
|
||||
return code - 71
|
||||
}
|
||||
if (code >= 48 && code <= 57) {
|
||||
return code + 4
|
||||
}
|
||||
if (code === 43) {
|
||||
return 62
|
||||
}
|
||||
if (code === 47) {
|
||||
return 63
|
||||
}
|
||||
if (code === 61) {
|
||||
return BASE64_PADDING
|
||||
}
|
||||
return INVALID_BASE64
|
||||
}
|
||||
|
||||
function isWhitespace(code: number): boolean {
|
||||
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32
|
||||
}
|
||||
|
||||
function referenceWriteQuartet(
|
||||
output: Uint8Array,
|
||||
offset: number,
|
||||
quartet: readonly number[]
|
||||
): { bytesWritten: number; padded: boolean } | null {
|
||||
const [a, b, c, d] = quartet
|
||||
if (a === undefined || b === undefined || a < 0 || b < 0) {
|
||||
return null
|
||||
}
|
||||
if (c === BASE64_PADDING) {
|
||||
if (d !== BASE64_PADDING) {
|
||||
return null
|
||||
}
|
||||
if (offset < output.length) {
|
||||
output[offset] = (a << 2) | (b >> 4)
|
||||
}
|
||||
return { bytesWritten: Math.min(1, output.length - offset), padded: true }
|
||||
}
|
||||
if (c === undefined || c < 0) {
|
||||
return null
|
||||
}
|
||||
if (offset < output.length) {
|
||||
output[offset] = (a << 2) | (b >> 4)
|
||||
}
|
||||
if (offset + 1 < output.length) {
|
||||
output[offset + 1] = ((b & 15) << 4) | (c >> 2)
|
||||
}
|
||||
if (d === BASE64_PADDING) {
|
||||
return { bytesWritten: Math.min(2, output.length - offset), padded: true }
|
||||
}
|
||||
if (d === undefined || d < 0) {
|
||||
return null
|
||||
}
|
||||
if (offset + 2 < output.length) {
|
||||
output[offset + 2] = ((c & 3) << 6) | d
|
||||
}
|
||||
return { bytesWritten: Math.min(3, output.length - offset), padded: false }
|
||||
}
|
||||
|
||||
function referenceDecodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
|
||||
const capacity = Math.min(maxBytes, Math.ceil(content.length / 4) * 3)
|
||||
const output = new Uint8Array(capacity)
|
||||
const quartet: number[] = []
|
||||
let outputLength = 0
|
||||
let padded = false
|
||||
|
||||
for (let index = 0; index < content.length && outputLength < capacity; index += 1) {
|
||||
const code = content.charCodeAt(index)
|
||||
if (isWhitespace(code)) {
|
||||
continue
|
||||
}
|
||||
if (padded) {
|
||||
return null
|
||||
}
|
||||
const value = base64Value(code)
|
||||
if (value === INVALID_BASE64) {
|
||||
return null
|
||||
}
|
||||
quartet.push(value)
|
||||
if (quartet.length !== 4) {
|
||||
continue
|
||||
}
|
||||
const decoded = referenceWriteQuartet(output, outputLength, quartet)
|
||||
if (!decoded) {
|
||||
return null
|
||||
}
|
||||
outputLength += decoded.bytesWritten
|
||||
padded = decoded.padded
|
||||
quartet.length = 0
|
||||
}
|
||||
|
||||
if (!padded && outputLength < capacity && quartet.length > 0) {
|
||||
if (quartet.length === 1 || quartet.includes(BASE64_PADDING)) {
|
||||
return null
|
||||
}
|
||||
while (quartet.length < 4) {
|
||||
quartet.push(BASE64_PADDING)
|
||||
}
|
||||
const decoded = referenceWriteQuartet(output, outputLength, quartet)
|
||||
if (!decoded) {
|
||||
return null
|
||||
}
|
||||
outputLength += decoded.bytesWritten
|
||||
}
|
||||
return output.subarray(0, outputLength)
|
||||
}
|
||||
|
||||
function pngBytes(totalBytes: number, width: number, height: number): Buffer {
|
||||
const bytes = Buffer.alloc(Math.max(totalBytes, 24))
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(bytes)
|
||||
@@ -191,89 +56,7 @@ function jpegBytes(
|
||||
return Buffer.concat(parts)
|
||||
}
|
||||
|
||||
function gifBytes(width: number, height: number): Buffer {
|
||||
const gif = Buffer.alloc(64)
|
||||
gif.write('GIF89a', 0, 'ascii')
|
||||
gif.writeUInt16LE(width, 6)
|
||||
gif.writeUInt16LE(height, 8)
|
||||
return gif
|
||||
}
|
||||
|
||||
function webpBytes(width: number, height: number): Buffer {
|
||||
const webp = Buffer.alloc(64)
|
||||
webp.write('RIFF', 0, 'ascii')
|
||||
webp.writeUInt32LE(50, 4)
|
||||
webp.write('WEBP', 8, 'ascii')
|
||||
webp.write('VP8X', 12, 'ascii')
|
||||
webp.writeUInt32LE(10, 16)
|
||||
webp.writeUIntLE(width - 1, 24, 3)
|
||||
webp.writeUIntLE(height - 1, 27, 3)
|
||||
return webp
|
||||
}
|
||||
|
||||
const DECODE_FIXTURES: { label: string; content: string }[] = [
|
||||
{ label: 'png', content: pngBytes(24, 512, 512).toString('base64') },
|
||||
{ label: 'png padded once', content: pngBytes(26, 512, 512).toString('base64') },
|
||||
{ label: 'png padded twice', content: pngBytes(25, 512, 512).toString('base64') },
|
||||
{ label: 'png 70 KiB', content: pngBytes(70_000, 512, 512).toString('base64') },
|
||||
{ label: 'jpeg', content: jpegBytes(0, 640, 480).toString('base64') },
|
||||
{ label: 'jpeg 70 KiB exif', content: jpegBytes(70_000, 4000, 3000).toString('base64') },
|
||||
{ label: 'gif', content: gifBytes(320, 240).toString('base64') },
|
||||
{ label: 'webp', content: webpBytes(800, 600).toString('base64') },
|
||||
{ label: 'empty', content: '' },
|
||||
{ label: 'one character', content: 'A' },
|
||||
{ label: 'two characters', content: 'AB' },
|
||||
{ label: 'three characters', content: 'ABC' },
|
||||
{ label: 'invalid character', content: 'AB*D' },
|
||||
{ label: 'invalid tail', content: `${pngBytes(24, 4, 4).toString('base64')}!!!` },
|
||||
{ label: 'padding mid-payload', content: 'AAAA=AAA' },
|
||||
{ label: 'lone padding in tail', content: 'AAAAAB=' },
|
||||
{ label: 'single padding', content: 'AAAAAA==' },
|
||||
{ label: 'double padding', content: 'AAAAAAA=' },
|
||||
{ label: 'stray padding after padding', content: 'AAAA====' },
|
||||
{
|
||||
label: 'line-wrapped png',
|
||||
content: pngBytes(70_000, 512, 512)
|
||||
.toString('base64')
|
||||
.replace(/(.{76})/g, '$1\r\n')
|
||||
},
|
||||
{ label: 'leading and trailing whitespace', content: `\n\t ${'AAAA'} \r\n` },
|
||||
{ label: 'truncated png header', content: pngBytes(24, 512, 512).toString('base64').slice(0, 18) }
|
||||
]
|
||||
|
||||
const DECODE_CAPS = [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
1024,
|
||||
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
|
||||
]
|
||||
|
||||
describe('decodeBase64Prefix', () => {
|
||||
it('decodes byte-for-byte identically to the reference implementation', () => {
|
||||
for (const { label, content } of DECODE_FIXTURES) {
|
||||
for (const maxBytes of DECODE_CAPS) {
|
||||
const expected = referenceDecodeBase64Prefix(content, maxBytes)
|
||||
const actual = decodeBase64Prefix(content, maxBytes)
|
||||
const detail = `${label} @ maxBytes=${maxBytes}`
|
||||
if (expected === null) {
|
||||
expect(actual, detail).toBeNull()
|
||||
continue
|
||||
}
|
||||
expect(actual, detail).not.toBeNull()
|
||||
expect(Array.from(actual!), detail).toEqual(Array.from(expected))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('stops at the byte cap instead of decoding the whole payload', () => {
|
||||
const content = pngBytes(70_000, 512, 512).toString('base64')
|
||||
expect(decodeBase64Prefix(content, 32)?.byteLength).toBe(32)
|
||||
@@ -311,30 +94,4 @@ describe('exceedsRasterImagePreviewLimits', () => {
|
||||
const content = `${pngBytes(70_000, 32_769, 1).toString('base64')}!!!`
|
||||
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(false)
|
||||
})
|
||||
|
||||
it('agrees with the unprobed implementation on every fixture and mime type', () => {
|
||||
const mimeTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
undefined
|
||||
]
|
||||
const fixtures = [
|
||||
...DECODE_FIXTURES,
|
||||
{ label: 'over-limit png', content: pngBytes(24, 32_769, 1).toString('base64') },
|
||||
{ label: 'over-limit pixels png', content: pngBytes(24, 8192, 8192).toString('base64') },
|
||||
{ label: 'over-limit gif', content: gifBytes(65_535, 65_535).toString('base64') },
|
||||
{ label: 'over-limit jpeg', content: jpegBytes(70_000, 40_000, 40_000).toString('base64') },
|
||||
{ label: 'over-limit webp', content: webpBytes(40_000, 40_000).toString('base64') }
|
||||
]
|
||||
for (const { label, content } of fixtures) {
|
||||
for (const mimeType of mimeTypes) {
|
||||
expect(exceedsRasterImagePreviewLimits(content, mimeType), `${label} / ${mimeType}`).toBe(
|
||||
unprobedExceeds(content, mimeType)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,16 +15,6 @@ describe('react update depth attribution', () => {
|
||||
).toBe(UNRELIABLE_BOUNDARY_ATTRIBUTION)
|
||||
})
|
||||
|
||||
it('matches the legacy error-decoder invariant form', () => {
|
||||
expect(
|
||||
getReactErrorBoundaryAttribution(
|
||||
new Error(
|
||||
'Minified React error #185; visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message.'
|
||||
)
|
||||
)
|
||||
).toBe(UNRELIABLE_BOUNDARY_ATTRIBUTION)
|
||||
})
|
||||
|
||||
it('matches the development message', () => {
|
||||
expect(
|
||||
getReactErrorBoundaryAttribution(new Error('Maximum update depth exceeded. This can happen…'))
|
||||
|
||||
@@ -2,15 +2,12 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
findInstallerAssetName,
|
||||
getReleaseNotesUrlForVersion,
|
||||
getReleaseRepoForChannel,
|
||||
getVersionChannel,
|
||||
hasDedicatedReleaseRepo,
|
||||
hasInstallableArtifactForPlatform,
|
||||
isAdhocVersion,
|
||||
isChannelSupportedOnPlatform,
|
||||
isDailyVersion,
|
||||
isHourlyVersion,
|
||||
isReleaseChannel,
|
||||
parseAdhocVersionStamp,
|
||||
parseDailyVersionStamp,
|
||||
parseDevBuildStamp,
|
||||
@@ -33,24 +30,6 @@ describe('release channel', () => {
|
||||
expect(getVersionChannel('not-a-version')).toBeNull()
|
||||
})
|
||||
|
||||
// The main release Atom feed holds only 10 entries; daily/hourly tags would evict stable/RC updates.
|
||||
it('keeps dev builds out of the main release repo, and apart from each other', () => {
|
||||
expect(getReleaseRepoForChannel('hourly')).toBe('stablyai/orca-hourly')
|
||||
expect(getReleaseRepoForChannel('daily')).toBe('stablyai/orca-daily')
|
||||
// Keep unlanded adhoc branch builds away from users following main’s hourly channel.
|
||||
expect(getReleaseRepoForChannel('adhoc')).toBe('stablyai/orca-adhoc')
|
||||
expect(getReleaseRepoForChannel('stable')).toBe('stablyai/orca')
|
||||
expect(getReleaseRepoForChannel('rc')).toBe('stablyai/orca')
|
||||
})
|
||||
|
||||
it('marks exactly the dev channels as having their own repo', () => {
|
||||
expect(hasDedicatedReleaseRepo('hourly')).toBe(true)
|
||||
expect(hasDedicatedReleaseRepo('daily')).toBe(true)
|
||||
expect(hasDedicatedReleaseRepo('adhoc')).toBe(true)
|
||||
expect(hasDedicatedReleaseRepo('stable')).toBe(false)
|
||||
expect(hasDedicatedReleaseRepo('rc')).toBe(false)
|
||||
})
|
||||
|
||||
// Dev tags exist only in their dedicated repos; main-repo links 404.
|
||||
it('builds release-notes links against the repo that published the version', () => {
|
||||
expect(getReleaseNotesUrlForVersion('1.4.160-hourly.202607281400')).toBe(
|
||||
@@ -221,16 +200,6 @@ describe('release channel', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts only known channels', () => {
|
||||
expect(isReleaseChannel('hourly')).toBe(true)
|
||||
expect(isReleaseChannel('daily')).toBe(true)
|
||||
expect(isReleaseChannel('adhoc')).toBe(true)
|
||||
expect(isReleaseChannel('stable')).toBe(true)
|
||||
expect(isReleaseChannel('nightly')).toBe(false)
|
||||
expect(isReleaseChannel(null)).toBe(false)
|
||||
expect(isReleaseChannel(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
// Timestamp tails distinguish consecutive hourlies; keep picker ordering chronological.
|
||||
it('sorts consecutive hourly builds newest first', () => {
|
||||
const build = (version: string): ReleaseBuild => ({
|
||||
|
||||
@@ -18,12 +18,7 @@ import {
|
||||
import { sendRemoteRuntimeRequest, subscribeRemoteRuntimeRequest } from './remote-runtime-client'
|
||||
import { remoteRuntimeClientCapabilities } from './remote-runtime-client-capabilities'
|
||||
import { MAX_TIMER_DELAY_MS } from './timer-delay'
|
||||
import {
|
||||
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
|
||||
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY,
|
||||
BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY
|
||||
} from './protocol-version'
|
||||
import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from './electron-remote-runtime-client-capabilities'
|
||||
import { BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY } from './protocol-version'
|
||||
|
||||
const servers: WebSocketServer[] = []
|
||||
|
||||
@@ -189,39 +184,6 @@ describe('subscribeRemoteRuntimeRequest', () => {
|
||||
await vi.waitFor(() => expect(releaseSocket).toHaveBeenCalledOnce())
|
||||
})
|
||||
|
||||
it('detaches subscription socket listeners after close', async () => {
|
||||
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
|
||||
try {
|
||||
const server = await createSubscriptionServer()
|
||||
const onResponse = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
const subscription = await subscribeRemoteRuntimeRequest(
|
||||
server.pairing,
|
||||
'terminal.subscribe',
|
||||
{ terminal: 't1' },
|
||||
1000,
|
||||
{
|
||||
onResponse,
|
||||
onError,
|
||||
onClose
|
||||
}
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
|
||||
subscription.close()
|
||||
await vi.waitFor(() => expect(onClose).toHaveBeenCalledOnce())
|
||||
|
||||
const removedEvents = offSpy.mock.calls.map(([event]) => event)
|
||||
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
|
||||
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
|
||||
expect(onError).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
offSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('closes a half-open subscription socket via client liveness so callers can resubscribe', async () => {
|
||||
// Half-open dedicated streams get no close/pongs/data; client liveness must trigger onError/onClose so renderer resubscription runs (#7718).
|
||||
const server = await createSubscriptionServer({ disableAutoPong: true })
|
||||
@@ -288,52 +250,6 @@ describe('subscribeRemoteRuntimeRequest', () => {
|
||||
})
|
||||
|
||||
describe('sendRemoteRuntimeRequest', () => {
|
||||
it('keeps generic native authentication free of Electron placement support', async () => {
|
||||
let receivedAuth: Record<string, unknown> | null = null
|
||||
const server = await createOneShotServer({
|
||||
onAuth: (auth) => {
|
||||
receivedAuth = auth
|
||||
}
|
||||
})
|
||||
|
||||
await sendRemoteRuntimeRequest(
|
||||
server.pairing,
|
||||
'session.tabs.list',
|
||||
{ worktree: 'id:worktree-a' },
|
||||
1000
|
||||
)
|
||||
|
||||
expect(receivedAuth).toMatchObject({
|
||||
clientCapabilities: remoteRuntimeClientCapabilities()
|
||||
})
|
||||
})
|
||||
|
||||
it('advertises browser placement support only when the Electron caller opts in', async () => {
|
||||
let receivedAuth: Record<string, unknown> | null = null
|
||||
const server = await createOneShotServer({
|
||||
onAuth: (auth) => {
|
||||
receivedAuth = auth
|
||||
}
|
||||
})
|
||||
|
||||
await sendRemoteRuntimeRequest(
|
||||
server.pairing,
|
||||
'session.tabs.list',
|
||||
{ worktree: 'id:worktree-a' },
|
||||
1000,
|
||||
undefined,
|
||||
undefined,
|
||||
ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES
|
||||
)
|
||||
|
||||
expect(receivedAuth).toMatchObject({
|
||||
clientCapabilities: expect.arrayContaining([
|
||||
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
|
||||
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it.each([-1, 1.5, MAX_TIMER_DELAY_MS + 1, Number.MAX_SAFE_INTEGER + 1])(
|
||||
'rejects invalid timer delay %s before reading pairing data',
|
||||
async (timeoutMs) => {
|
||||
@@ -565,25 +481,6 @@ describe('sendRemoteRuntimeRequest', () => {
|
||||
expect(receivedRequest).not.toHaveProperty('orchestrationContractVersion')
|
||||
expect(receivedRequest).not.toHaveProperty('orchestrationRequestId')
|
||||
})
|
||||
|
||||
it('detaches one-shot socket listeners after a successful response', async () => {
|
||||
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
|
||||
try {
|
||||
const server = await createOneShotServer()
|
||||
|
||||
await sendRemoteRuntimeRequest<{ satisfied: boolean }>(
|
||||
server.pairing,
|
||||
'terminal.wait',
|
||||
{ terminal: 't1', for: 'tui-idle', timeoutMs: 550 },
|
||||
300
|
||||
)
|
||||
|
||||
const removedEvents = offSpy.mock.calls.map(([event]) => event)
|
||||
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
|
||||
} finally {
|
||||
offSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function createSubscriptionServer(
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
probeAllRemoteRuntimeSocketsNow,
|
||||
startRemoteRuntimeSocketLiveness
|
||||
} from './remote-runtime-socket-liveness'
|
||||
import { SharedControlReconnectScheduler } from './remote-runtime/control/remote-runtime-shared-control-reconnect'
|
||||
|
||||
// After sleep, probe every socket immediately; 8s misses only re-ping and the normal 25s keepalive decides death so relayed paths have time to recover (#9092).
|
||||
describe('remote runtime resume probe', () => {
|
||||
@@ -154,45 +153,3 @@ describe('remote runtime resume probe', () => {
|
||||
expect(onDead).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SharedControlReconnectScheduler.retryNow after an outage', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('restarts the backoff ladder so a failure right after wake retries quickly', () => {
|
||||
const scheduler = new SharedControlReconnectScheduler()
|
||||
const open = vi.fn()
|
||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||
scheduler.scheduleWithIdleBackoff(false, open)
|
||||
vi.runOnlyPendingTimers()
|
||||
}
|
||||
expect(scheduler.attemptCount).toBeGreaterThanOrEqual(6)
|
||||
|
||||
scheduler.scheduleWithIdleBackoff(false, open)
|
||||
expect(scheduler.retryNow()).toBe(true)
|
||||
expect(scheduler.attemptCount).toBe(0)
|
||||
})
|
||||
|
||||
it('restarts the ladder even when the wake lands while a reconnect is already opening', () => {
|
||||
const scheduler = new SharedControlReconnectScheduler()
|
||||
const open = vi.fn()
|
||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||
scheduler.scheduleWithIdleBackoff(false, open)
|
||||
vi.runOnlyPendingTimers()
|
||||
}
|
||||
// The last open() is in flight, so no timer is pending for retryNow to advance.
|
||||
expect(scheduler.isScheduled).toBe(false)
|
||||
expect(scheduler.retryNow()).toBe(false)
|
||||
expect(scheduler.attemptCount).toBe(0)
|
||||
})
|
||||
|
||||
it('does nothing when no reconnect is pending', () => {
|
||||
const scheduler = new SharedControlReconnectScheduler()
|
||||
expect(scheduler.retryNow()).toBe(false)
|
||||
expect(scheduler.attemptCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Buffer } from 'node:buffer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from './remote-runtime-client-error'
|
||||
import {
|
||||
RECOVERABLE_CODES,
|
||||
RECOVERABLE_MESSAGE_FRAGMENTS,
|
||||
isRecoverableRemoteRuntimeConnectionError,
|
||||
isRuntimeRpcQueueOverloadError,
|
||||
@@ -411,12 +410,6 @@ function classifyByMessageOnly(pair: TransportErrorPair): boolean {
|
||||
}
|
||||
|
||||
describe('transport error code/message classification agreement', () => {
|
||||
it('enumerates every reachable coded producer', () => {
|
||||
// Use a floor so corpus additions pass; lower deliberately only for genuinely removed producers (#12667 originally enumerated 34).
|
||||
expect(CODED_TRANSPORT_ERRORS.length).toBeGreaterThanOrEqual(59)
|
||||
expect(CODED_TRANSPORT_ERRORS.every((pair) => typeof pair.code === 'string')).toBe(true)
|
||||
})
|
||||
|
||||
it('never classifies a coded error as fatal while its own message reads as recoverable', () => {
|
||||
const violations = CODED_TRANSPORT_ERRORS.filter(
|
||||
(pair) => !isRecoverableRemoteRuntimeConnectionError(pair) && classifyByMessageOnly(pair)
|
||||
@@ -450,33 +443,6 @@ describe('transport error code/message classification agreement', () => {
|
||||
expect(misclassified).toEqual([])
|
||||
})
|
||||
|
||||
it('backs every recoverable message fragment with a producer in the corpus', () => {
|
||||
const allMessages = [...CODED_TRANSPORT_ERRORS, ...CODELESS_TRANSPORT_ERRORS].map((pair) =>
|
||||
pair.message.toLowerCase()
|
||||
)
|
||||
const unbacked = RECOVERABLE_MESSAGE_FRAGMENTS.filter(
|
||||
(fragment) => !allMessages.some((message) => message.includes(fragment))
|
||||
).map(
|
||||
(fragment) =>
|
||||
`recoverable fragment "${fragment}" matches no producer in this corpus — add the producer that emits it, or drop the fragment.`
|
||||
)
|
||||
expect(unbacked).toEqual([])
|
||||
})
|
||||
|
||||
it('backs every recoverable code with a producer in the corpus', () => {
|
||||
// 'reconnecting' is vocabulary borrowed from the SSH/runtime status states; no error
|
||||
// producer raises it. Kept recoverable defensively.
|
||||
const codesWithoutProducer = new Set(['reconnecting'])
|
||||
const corpusCodes = new Set(CODED_TRANSPORT_ERRORS.map((pair) => pair.code))
|
||||
const unbacked = [...RECOVERABLE_CODES]
|
||||
.filter((code) => !corpusCodes.has(code) && !codesWithoutProducer.has(code))
|
||||
.map(
|
||||
(code) =>
|
||||
`recoverable code "${code}" matches no producer in this corpus — add the producer that raises it so its message is checked, or declare it producer-less here.`
|
||||
)
|
||||
expect(unbacked).toEqual([])
|
||||
})
|
||||
|
||||
it('classifies remote_runtime_busy recoverable with or without its code (STA-3479)', () => {
|
||||
for (const pair of BUSY_TRANSPORT_ERRORS) {
|
||||
expect(isRecoverableRemoteRuntimeConnectionError(pair)).toBe(true)
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { getRemoteRuntimeRequestAdmissionEvidence } from '../requests/remote-runtime-prepared-request-admission'
|
||||
import { remoteRuntimeClientCapabilities } from '../../remote-runtime-client-capabilities'
|
||||
import { RemoteRuntimeSharedControlConnection } from './remote-runtime-shared-control-connection'
|
||||
import * as sharedControlProtocol from './remote-runtime-shared-control-protocol'
|
||||
import { isRuntimeSubscriptionReplayResponse } from '../../runtime-subscription-replay'
|
||||
import {
|
||||
closeSharedControlTestServers,
|
||||
@@ -81,10 +80,6 @@ describe('RemoteRuntimeSharedControlConnection', () => {
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('does not expose a binary sender on the shared control protocol surface', () => {
|
||||
expect('sendSharedControlEncryptedBinary' in sharedControlProtocol).toBe(false)
|
||||
})
|
||||
|
||||
it('releases a pending request when the socket send throws', async () => {
|
||||
const connection = new RemoteRuntimeSharedControlConnection(
|
||||
pairedRuntimeConnectionFromOffer({
|
||||
@@ -122,36 +117,6 @@ describe('RemoteRuntimeSharedControlConnection', () => {
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('replaces a stuck pre-ready socket when a one-shot probe proves reachability', () => {
|
||||
const connection = new RemoteRuntimeSharedControlConnection(
|
||||
pairedRuntimeConnectionFromOffer({
|
||||
v: 2,
|
||||
endpoint: 'ws://127.0.0.1:1',
|
||||
deviceToken: 'token',
|
||||
publicKeyB64: Buffer.from(new Uint8Array(32).fill(1)).toString('base64')
|
||||
})
|
||||
)
|
||||
const close = vi.fn()
|
||||
const cleanup = vi.fn()
|
||||
const open = vi.fn()
|
||||
const unsafe = connection as unknown as {
|
||||
state: string
|
||||
ws: { readyState: number; close: () => void } | null
|
||||
socketCleanup: (() => void) | null
|
||||
open: () => void
|
||||
}
|
||||
unsafe.state = 'awaiting_ready'
|
||||
unsafe.ws = { readyState: 0, close }
|
||||
unsafe.socketCleanup = cleanup
|
||||
unsafe.open = open
|
||||
|
||||
connection.reconnectNow()
|
||||
|
||||
expect(cleanup).toHaveBeenCalledOnce()
|
||||
expect(close).toHaveBeenCalledOnce()
|
||||
expect(open).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps a waiting request alive when a reachability probe replaces its pre-ready socket', async () => {
|
||||
const server = await createServer({ suppressReadyFrameCount: 1 })
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
@@ -257,29 +222,6 @@ describe('RemoteRuntimeSharedControlConnection', () => {
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('keeps many logical subscriptions on one authenticated WebSocket', async () => {
|
||||
const server = await createServer()
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
const subscriptions = await Promise.all(
|
||||
Array.from({ length: 35 }, (_value, index) =>
|
||||
connection.subscribe('runtime.clientEvents.subscribe', { index }, 1000, {
|
||||
onResponse: vi.fn(),
|
||||
onError: vi.fn()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(server.requests).toHaveLength(35))
|
||||
|
||||
expect(server.connectionCount()).toBe(1)
|
||||
expect(
|
||||
server.requests.every((request) => request.method === 'runtime.clientEvents.subscribe')
|
||||
).toBe(true)
|
||||
|
||||
subscriptions.forEach((subscription) => subscription.close())
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('reconnects and replays passive subscriptions without closing them', async () => {
|
||||
const server = await createServer({ closeAfterFirstStreamingResponse: true })
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
@@ -310,74 +252,6 @@ describe('RemoteRuntimeSharedControlConnection', () => {
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('keeps passive subscriptions alive after reaching the capped reconnect delay', async () => {
|
||||
const server = await createServer()
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
const onClose = vi.fn()
|
||||
|
||||
const unsafe = connection as unknown as {
|
||||
reconnect: {
|
||||
attempt: number
|
||||
scheduleWithDefaultBackoff: (intentionallyClosed: boolean, open: () => void) => void
|
||||
}
|
||||
subscriptions: Map<string, unknown>
|
||||
}
|
||||
unsafe.reconnect.attempt = 7
|
||||
unsafe.subscriptions.set('sub-1', {
|
||||
requestId: 'sub-1',
|
||||
method: 'runtime.clientEvents.subscribe',
|
||||
params: null,
|
||||
callbacks: { onResponse: vi.fn(), onError: vi.fn(), onClose },
|
||||
sent: false,
|
||||
closed: false,
|
||||
closeAfterReady: false,
|
||||
remoteSubscriptionId: null
|
||||
})
|
||||
|
||||
unsafe.reconnect.scheduleWithDefaultBackoff(false, () => {})
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
expect(connection.getDiagnostics()).toMatchObject({
|
||||
state: 'reconnecting',
|
||||
reconnectAttempt: 8,
|
||||
subscriptionCount: 1
|
||||
})
|
||||
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('resets reconnect attempts after a stable authenticated ready period', async () => {
|
||||
const server = await createServer()
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing, {
|
||||
reconnectStableResetMs: 50
|
||||
})
|
||||
|
||||
await expect(connection.request('worktree.ps', undefined, 1000)).resolves.toMatchObject({
|
||||
ok: true
|
||||
})
|
||||
;(connection as unknown as { reconnect: { attempt: number } }).reconnect.attempt = 3
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(connection.getDiagnostics()).toMatchObject({ reconnectAttempt: 0 })
|
||||
)
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('removes ready waiters when a one-shot request times out during handshake', async () => {
|
||||
const server = await createServer({ suppressReadyFrame: true })
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
const unsafe = connection as unknown as {
|
||||
readyWaiters: unknown[]
|
||||
pendingRequests: Map<string, unknown>
|
||||
}
|
||||
|
||||
await expect(connection.request('worktree.ps', undefined, 25)).rejects.toThrow('Timed out')
|
||||
|
||||
await vi.waitFor(() => expect(unsafe.readyWaiters).toHaveLength(0))
|
||||
expect(unsafe.pendingRequests.size).toBe(0)
|
||||
connection.close()
|
||||
})
|
||||
|
||||
it('cleans up an id-scoped subscription closed before its ready response', async () => {
|
||||
const server = await createServer({ delaySubscriptionReady: true })
|
||||
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
|
||||
|
||||
-9
@@ -74,15 +74,6 @@ describe('shared control keepalive timeout refresh semantics', () => {
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
it('fires the deadline for a short RPC when no keepalives arrive', async () => {
|
||||
const { pendingRequests, promise } = startRequest()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1001)
|
||||
|
||||
await expect(promise).rejects.toThrow()
|
||||
expect(pendingRequests.size).toBe(0)
|
||||
})
|
||||
|
||||
it('aborts one pending request without disturbing its peer', async () => {
|
||||
const pendingRequests = new Map<string, SharedControlPendingRequest<unknown>>()
|
||||
const controller = new AbortController()
|
||||
|
||||
-25
@@ -92,31 +92,6 @@ describe('shared-control standing intent', () => {
|
||||
scheduler.clear()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ manual: false, paused: false, subscriptions: 0, scheduled: true },
|
||||
{ manual: false, paused: false, subscriptions: 1, scheduled: true },
|
||||
{ manual: false, paused: true, subscriptions: 0, scheduled: false },
|
||||
{ manual: false, paused: true, subscriptions: 1, scheduled: true },
|
||||
{ manual: true, paused: false, subscriptions: 0, scheduled: false },
|
||||
{ manual: true, paused: false, subscriptions: 1, scheduled: false },
|
||||
{ manual: true, paused: true, subscriptions: 0, scheduled: false },
|
||||
{ manual: true, paused: true, subscriptions: 1, scheduled: false }
|
||||
])(
|
||||
'applies the intent/capability truth table: $manual/$paused/$subscriptions',
|
||||
({ manual, paused, subscriptions, scheduled }) => {
|
||||
vi.useFakeTimers()
|
||||
const connection = testConnection({ manual, paused })
|
||||
if (subscriptions > 0) {
|
||||
addLogicalSubscription(connection)
|
||||
}
|
||||
|
||||
closeCurrentGeneration(connection)
|
||||
|
||||
expect(connection.getDiagnostics().state === 'reconnecting').toBe(scheduled)
|
||||
connection.close()
|
||||
}
|
||||
)
|
||||
|
||||
it('advances an armed idle retry without recreating cleared work', () => {
|
||||
vi.useFakeTimers()
|
||||
const connection = testConnection()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
RECOVERABLE_CODES,
|
||||
RECOVERABLE_MESSAGE_FRAGMENTS,
|
||||
isRecoverableRemoteRuntimeConnectionError,
|
||||
isRuntimeRpcQueueOverloadError,
|
||||
toRemoteRuntimeClientErrorLike
|
||||
@@ -64,33 +62,6 @@ describe('remote runtime client error classification', () => {
|
||||
expect(isRecoverableRemoteRuntimeConnectionError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('pins the recoverable codes and message fragments', () => {
|
||||
expect([...RECOVERABLE_CODES].toSorted()).toEqual([
|
||||
'reconnecting',
|
||||
'remote_runtime_busy',
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'runtime_timeout',
|
||||
'runtime_unavailable',
|
||||
'timeout'
|
||||
])
|
||||
expect(RECOVERABLE_MESSAGE_FRAGMENTS).toEqual([
|
||||
'could not connect to the remote orca runtime',
|
||||
'remote orca runtime closed the connection',
|
||||
'remote orca runtime connection closed',
|
||||
'remote orca runtime is not connected',
|
||||
'remote runtime call queue is full',
|
||||
'remote runtime connection closed',
|
||||
'remote runtime connection wait limit reached',
|
||||
'remote runtime request limit reached',
|
||||
'remote runtime subscription limit reached',
|
||||
'remote runtime subscription memory limit reached',
|
||||
'remote runtime subscription closed before it started',
|
||||
'remote terminal stream is not connected',
|
||||
'timed out waiting for the remote orca runtime'
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'ssh_user_disconnected',
|
||||
@@ -99,17 +70,4 @@ describe('remote runtime client error classification', () => {
|
||||
])('does not treat client code %s as recoverable', (code) => {
|
||||
expect(isRecoverableRemoteRuntimeConnectionError({ code, message: 'refused' })).toBe(false)
|
||||
})
|
||||
|
||||
it.each(RECOVERABLE_MESSAGE_FRAGMENTS)(
|
||||
'recovers a code-stripped message containing %s',
|
||||
(fragment) => {
|
||||
expect(
|
||||
isRecoverableRemoteRuntimeConnectionError(
|
||||
toRemoteRuntimeClientErrorLike(
|
||||
new Error(`Error invoking remote method 'x': Error: ${fragment}.`)
|
||||
)
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -136,27 +136,6 @@ describe('createUpdaterQuitAbortRelay', () => {
|
||||
expect(aborted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('resets a prepared restart on a linux package-install recovery status', () => {
|
||||
const eventTarget = new EventTarget()
|
||||
const aborted = vi.fn()
|
||||
eventTarget.addEventListener('update-restart-aborted', aborted)
|
||||
const relay = createUpdaterQuitAbortRelay(eventTarget, 'update-restart-aborted')
|
||||
relay.markPrepared()
|
||||
|
||||
relay.handleStatus({
|
||||
state: 'error',
|
||||
message: 'No authentication agent found.',
|
||||
recovery: {
|
||||
kind: 'linux-package-install',
|
||||
packageType: 'deb',
|
||||
reason: 'authentication-agent-unavailable',
|
||||
version: '1.0.61'
|
||||
}
|
||||
} satisfies UpdateStatus)
|
||||
|
||||
expect(aborted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('ignores updater errors when no update restart was prepared', () => {
|
||||
const eventTarget = new EventTarget()
|
||||
const aborted = vi.fn()
|
||||
|
||||
@@ -2,18 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from './repo-display-labels'
|
||||
|
||||
describe('getRepoDisplayLabelsByPath', () => {
|
||||
it('keeps non-colliding repository names basename-only', () => {
|
||||
const labels = getRepoDisplayLabelsByPath([
|
||||
{ path: '/workspace/platform/web', displayName: 'web' },
|
||||
{ path: '/workspace/platform/worker', displayName: 'worker' }
|
||||
])
|
||||
|
||||
expect(labels.get(getRepoDisplayLabelKey({ path: '/workspace/platform/web' }))).toBe('web')
|
||||
expect(labels.get(getRepoDisplayLabelKey({ path: '/workspace/platform/worker' }))).toBe(
|
||||
'worker'
|
||||
)
|
||||
})
|
||||
|
||||
it('adds the minimal real parent suffix only for colliding basenames', () => {
|
||||
const labels = getRepoDisplayLabelsByPath([
|
||||
{ path: '/workspace/platform/web', displayName: 'web' },
|
||||
@@ -80,15 +68,6 @@ describe('getRepoDisplayLabelsByPath', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps identical project paths short across local and paired execution hosts', () => {
|
||||
const local = { path: '/home/user/projects/app', displayName: 'app' }
|
||||
const paired = { ...local, executionHostId: 'runtime:home' as const }
|
||||
const labels = getRepoDisplayLabelsByPath([local, paired])
|
||||
expect(labels.get(getRepoDisplayLabelKey(local))).toBe('app')
|
||||
expect(labels.get(getRepoDisplayLabelKey(paired))).toBe('app')
|
||||
expect(labels.size).toBe(2)
|
||||
})
|
||||
|
||||
it('disambiguates collisions only within their owning host, even when paths overlap', () => {
|
||||
const local = { path: '/workspace/payments/api', displayName: 'api' }
|
||||
const sibling = { path: '/workspace/billing/api', displayName: 'api' }
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toRuntimeExecutionHostId, toSshExecutionHostId } from './execution-host'
|
||||
import {
|
||||
getRepoHostIdentityForParts,
|
||||
getRepoRegistrationHostIds,
|
||||
type HostScopedRepoKey
|
||||
} from './repo-host-identity'
|
||||
import { getRepoHostIdentityForParts, getRepoRegistrationHostIds } from './repo-host-identity'
|
||||
|
||||
// Why: these keys index persisted and cross-module repo state, so the bytes are frozen (D25).
|
||||
describe('repo host identity golden bytes', () => {
|
||||
@@ -21,17 +17,6 @@ describe('repo host identity golden bytes', () => {
|
||||
] as const)('composes %s on %s', (repoId, hostId, expected) => {
|
||||
expect(getRepoHostIdentityForParts(repoId, hostId)).toBe(expected)
|
||||
})
|
||||
|
||||
it('brands the key so a plain string cannot pose as one', () => {
|
||||
const key = getRepoHostIdentityForParts('repo-1', 'local')
|
||||
expectTypeOf(key).toEqualTypeOf<HostScopedRepoKey>()
|
||||
expectTypeOf<HostScopedRepoKey>().toExtend<string>()
|
||||
expectTypeOf<string>().not.toExtend<HostScopedRepoKey>()
|
||||
const raw: string = key
|
||||
// @ts-expect-error a raw string is not a host-scoped key
|
||||
const forged: HostScopedRepoKey = 'local\0repo-1'
|
||||
expect(raw).toBe(forged)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRepoRegistrationHostIds', () => {
|
||||
|
||||
@@ -554,20 +554,6 @@ describe('RepoRefMaintenance single-flight and backoff', () => {
|
||||
expect(spans).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('records a failure when the pack left the backlog in place', async () => {
|
||||
const refs = await refsDirectoryWith(THRESHOLD + 2)
|
||||
const { maintenance, spans } = createHarness({
|
||||
packRefs: async () => {
|
||||
throw new Error('permission denied')
|
||||
}
|
||||
})
|
||||
|
||||
maintenance.arm(target('local::/denied/.git', refs, () => Promise.reject(new Error('denied'))))
|
||||
await elapseQuietPeriod(maintenance)
|
||||
|
||||
expect(attributesOf(spans[0])['repo.maintenance_outcome']).toBe('failed')
|
||||
})
|
||||
|
||||
it('records a failure instead of throwing, and backs off', async () => {
|
||||
const refs = await refsDirectoryWith(THRESHOLD + 2)
|
||||
const { maintenance, spans, packRefs } = createHarness({
|
||||
|
||||
@@ -22,18 +22,6 @@ describe('getRepoRoutingConnectionId', () => {
|
||||
expect(getRepoRoutingConnectionId(repo)).toBe(expected)
|
||||
})
|
||||
|
||||
it('never answers local for a row whose raw connectionId is set', () => {
|
||||
for (const executionHostId of [
|
||||
undefined,
|
||||
'local',
|
||||
'runtime:env',
|
||||
'ssh:gpu',
|
||||
'ssh:other'
|
||||
] as const) {
|
||||
expect(getRepoRoutingConnectionId({ executionHostId, connectionId: 'gpu' })).not.toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('lets executionHostId win on a contradictory SSH row, as getRepoExecutionHostId does', () => {
|
||||
expect(getRepoRoutingConnectionId({ executionHostId: 'ssh:b', connectionId: 'a' })).toBe('b')
|
||||
})
|
||||
|
||||
@@ -9,12 +9,10 @@ import {
|
||||
} from './agent-detection'
|
||||
import { isDecorativeAgentTitleFrameChange } from './agent-decorative-title-signature'
|
||||
import { stripLeadingAgentTitleDecoration } from './agent-title-decoration'
|
||||
import { resolveExplicitTerminalTitleAgentType } from './terminal-title-agent-type'
|
||||
|
||||
// Titles captured from real `claude` binaries running the same one-line prompt.
|
||||
// 2.1.228 swapped the busy spinner from braille to quarter circles, which read as
|
||||
// "no agent" and made the tracker report a confirmed exit mid-turn (#13889).
|
||||
const BUSY_2_1_227 = ['⠂ Claude Code', '⠐ Claude Code', '⠂ Say hi in one word'] as const
|
||||
const BUSY_2_1_228 = ['◐ Claude Code', '◑ Claude Code', '◑ Say hi in one word'] as const
|
||||
|
||||
const CAPTURED_2_1_228_TURN = [
|
||||
@@ -58,30 +56,12 @@ describe('Claude Code quarter-circle busy titles (#13889)', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('matches 2.1.227 status and identity frame for frame', () => {
|
||||
BUSY_2_1_228.forEach((title, index) => {
|
||||
const braille = BUSY_2_1_227[index]
|
||||
expect(detectAgentStatusFromTitle(title)).toBe(detectAgentStatusFromTitle(braille))
|
||||
expect(getAgentLabel(title)).toBe(getAgentLabel(braille))
|
||||
expect(resolveExplicitTerminalTitleAgentType(title)).toBe(
|
||||
resolveExplicitTerminalTitleAgentType(braille)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('never confirms an agent exit across a real 2.1.228 turn', () => {
|
||||
const events = trackTurn(CAPTURED_2_1_228_TURN)
|
||||
expect(events).not.toContain('exited')
|
||||
expect(events).toEqual(['working', 'idle'])
|
||||
})
|
||||
|
||||
it('tracks the 2.1.228 turn exactly like the 2.1.227 turn', () => {
|
||||
const brailleTurn = CAPTURED_2_1_228_TURN.map((title) =>
|
||||
title.replace('◐', '⠂').replace('◑', '⠐')
|
||||
)
|
||||
expect(trackTurn(CAPTURED_2_1_228_TURN)).toEqual(trackTurn(brailleTurn))
|
||||
})
|
||||
|
||||
it('strips the spinner from stale exit titles and displayed labels', () => {
|
||||
expect(clearWorkingIndicators('◐ Say hi in one word')).toBe('Say hi in one word')
|
||||
expect(stripLeadingAgentTitleDecoration('◐ Claude Code')).toBe('Claude Code')
|
||||
|
||||
@@ -51,22 +51,12 @@ describe('sharesWorktreeLineageBoundary', () => {
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('accepts the same host', () => {
|
||||
expect(sharesWorktreeLineageBoundary(boundary(), boundary())).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['child host', boundary({ hostId: undefined }), boundary()],
|
||||
['parent host', boundary(), boundary({ hostId: undefined })]
|
||||
])('treats an undefined %s as compatible', (_label, child, parent) => {
|
||||
expect(sharesWorktreeLineageBoundary(child, parent)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a different known host', () => {
|
||||
expect(sharesWorktreeLineageBoundary(boundary(), boundary({ hostId: 'ssh:remote' }))).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectResolvedWorktreeLineage', () => {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Opt-in Settings → Browser preview; enable with ORCA_SETTINGS_PREVIEW=1.
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
|
||||
test.skip(
|
||||
process.env.ORCA_SETTINGS_PREVIEW !== '1',
|
||||
'Preview only; run with ORCA_SETTINGS_PREVIEW=1'
|
||||
)
|
||||
|
||||
const HOLD_MINUTES = 20
|
||||
|
||||
test('shows the remote browsing settings section and holds for review', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}) => {
|
||||
test.setTimeout((HOLD_MINUTES + 10) * 60_000)
|
||||
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const window = BrowserWindow.getAllWindows()[0]
|
||||
window?.setSize(1440, 900)
|
||||
window?.center()
|
||||
window?.show()
|
||||
window?.focus()
|
||||
})
|
||||
|
||||
// Seed one opted-out SSH host so the "Route again" list renders too.
|
||||
await orcaPage.evaluate(() => {
|
||||
const state = window.__store?.getState()
|
||||
state?.updateSettings({ browserSshWorkspaceRoutingDisabledTargetIds: ['preview-target'] })
|
||||
window.__store?.setState({
|
||||
sshTargetLabels: new Map([['preview-target', 'openclaw']])
|
||||
} as never)
|
||||
state?.openSettingsTarget({
|
||||
pane: 'browser',
|
||||
repoId: null,
|
||||
sectionId: 'browser-ssh-workspace-routing'
|
||||
})
|
||||
state?.openSettingsPage()
|
||||
})
|
||||
|
||||
await expect(orcaPage.getByText('Remote browsing', { exact: true })).toBeVisible({
|
||||
timeout: 30_000
|
||||
})
|
||||
|
||||
console.log(`\n=== SETTINGS PREVIEW READY — window stays up ${HOLD_MINUTES} minutes ===\n`)
|
||||
await orcaPage.waitForTimeout(HOLD_MINUTES * 60_000)
|
||||
})
|
||||
@@ -410,56 +410,6 @@ test.describe('Browser Tab', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('Cmd/Ctrl+0 resets a zoomed browser page to 100%', async ({ orcaPage }) => {
|
||||
const formServer = await startBrowserFormServer()
|
||||
try {
|
||||
const worktreeId = (await getActiveWorktreeId(orcaPage))!
|
||||
const browserTab = await createBrowserTab(
|
||||
orcaPage,
|
||||
worktreeId,
|
||||
formServer.url('Zoom reset'),
|
||||
'Zoom Reset'
|
||||
)
|
||||
expect(browserTab?.id).toBeTruthy()
|
||||
await expect
|
||||
.poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 })
|
||||
.not.toBeNull()
|
||||
|
||||
const zoomModifier: 'meta' | 'control' = process.platform === 'darwin' ? 'meta' : 'control'
|
||||
await orcaPage.evaluate(
|
||||
async ({ browserTabId, browserPageId, modifier }) => {
|
||||
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
|
||||
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
|
||||
if (!webview) {
|
||||
throw new Error(`Missing webview for browser tab ${browserTabId}`)
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('orca:browser-page-zoom', {
|
||||
detail: { browserPageId, direction: 'in' }
|
||||
})
|
||||
)
|
||||
await webview.sendInputEvent({ type: 'keyDown', keyCode: '0', modifiers: [modifier] })
|
||||
await webview.sendInputEvent({ type: 'keyUp', keyCode: '0', modifiers: [modifier] })
|
||||
},
|
||||
{
|
||||
browserTabId: browserTab!.id,
|
||||
browserPageId: browserTab!.pageId ?? browserTab!.id,
|
||||
modifier: zoomModifier
|
||||
}
|
||||
)
|
||||
await expect
|
||||
.poll(() =>
|
||||
orcaPage.evaluate((browserTabId) => {
|
||||
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
|
||||
return (slot?.querySelector('webview') as Electron.WebviewTag | null)?.getZoomLevel()
|
||||
}, browserTab!.id)
|
||||
)
|
||||
.toBe(0)
|
||||
} finally {
|
||||
await formServer.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('reloading one browser tab does not adopt another tab zoom', async ({ orcaPage }) => {
|
||||
const [formServerA, formServerB] = await Promise.all([
|
||||
startBrowserFormServer(),
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
// Opt-in client-hosted browser tooltip preview; enable with ORCA_TOOLTIP_PREVIEW=1.
|
||||
import { createServer, type Server } from 'node:http'
|
||||
import type { AddressInfo } from 'node:net'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
|
||||
import { launchPairedElectronClient } from './helpers/paired-electron-client'
|
||||
|
||||
test.skip(process.env.ORCA_TOOLTIP_PREVIEW !== '1', 'Preview only; run with ORCA_TOOLTIP_PREVIEW=1')
|
||||
|
||||
const HOLD_MINUTES = 20
|
||||
|
||||
async function startFixtureServer(): Promise<{ close(): Promise<void>; url: string }> {
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
|
||||
response.end(
|
||||
'<!doctype html><html><head><title>tooltip-preview</title></head><body style="font-family:sans-serif;padding:2rem"><h1>Client-hosted page</h1><p>The intro tooltip should point at the controls above.</p></body></html>'
|
||||
)
|
||||
})
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
return {
|
||||
close: () => closeServer(server),
|
||||
url: `http://127.0.0.1:${(server.address() as AddressInfo).port}/preview`
|
||||
}
|
||||
}
|
||||
|
||||
async function closeServer(server: Server): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.closeAllConnections()
|
||||
server.close((error) => (error ? reject(error) : resolve()))
|
||||
})
|
||||
}
|
||||
|
||||
async function findPairedWorktreeId(page: Page, repoPath: string): Promise<string> {
|
||||
const read = () =>
|
||||
page.evaluate(
|
||||
(path) =>
|
||||
window.__store
|
||||
?.getState()
|
||||
.allWorktrees()
|
||||
.find((worktree) => worktree.path === path)?.id ?? null,
|
||||
repoPath
|
||||
)
|
||||
await expect
|
||||
.poll(read, { timeout: 60_000, message: 'paired client never received the host worktree' })
|
||||
.not.toBeNull()
|
||||
const worktreeId = await read()
|
||||
if (!worktreeId) {
|
||||
throw new Error('Paired worktree disappeared after discovery')
|
||||
}
|
||||
return worktreeId
|
||||
}
|
||||
|
||||
test('shows the client-hosted browser intro tooltip and holds for review', async ({
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
test.setTimeout((HOLD_MINUTES + 10) * 60_000)
|
||||
const fixtureServer = await startFixtureServer()
|
||||
const host = await launchHeadlessPairedRuntimeHost()
|
||||
let dispose = async (): Promise<void> => {
|
||||
await host.dispose()
|
||||
await fixtureServer.close()
|
||||
}
|
||||
try {
|
||||
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
|
||||
await host.client.call('terminal.create', {
|
||||
worktree: `path:${testRepoPath}`,
|
||||
title: 'Preview Terminal'
|
||||
})
|
||||
const client = await launchPairedElectronClient(host.offer, testInfo, 'tooltip preview')
|
||||
const disposeWithoutClient = dispose
|
||||
dispose = async () => {
|
||||
await client.dispose()
|
||||
await disposeWithoutClient()
|
||||
}
|
||||
|
||||
// The e2e profile launches hidden; surface the window for the human.
|
||||
await client.app.evaluate(({ BrowserWindow }) => {
|
||||
const window = BrowserWindow.getAllWindows()[0]
|
||||
window?.setSize(1440, 900)
|
||||
window?.center()
|
||||
window?.show()
|
||||
window?.focus()
|
||||
})
|
||||
|
||||
const worktreeId = await findPairedWorktreeId(client.page, testRepoPath)
|
||||
await client.page.evaluate(
|
||||
({ environmentId, worktreeId }) => {
|
||||
window.__store?.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`)
|
||||
},
|
||||
{ environmentId: client.environmentId, worktreeId }
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
(): Promise<number> =>
|
||||
client.page.evaluate(
|
||||
(id) => window.__store?.getState().groupsByWorktree[id]?.[0]?.tabOrder.length ?? 0,
|
||||
worktreeId
|
||||
),
|
||||
{ timeout: 120_000, message: 'paired client never adopted the host tab group' }
|
||||
)
|
||||
.toBeGreaterThanOrEqual(1)
|
||||
|
||||
await client.page.evaluate(
|
||||
async ({ url, worktreeId }) => {
|
||||
const state = window.__store?.getState()
|
||||
if (!state) {
|
||||
throw new Error('store unavailable')
|
||||
}
|
||||
const groupId = state.groupsByWorktree[worktreeId]?.[0]?.id
|
||||
state.setBrowserDefaultUrl(url)
|
||||
await state.openNewBrowserTabInActiveWorkspace(groupId)
|
||||
},
|
||||
{ url: fixtureServer.url, worktreeId }
|
||||
)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
(): Promise<string | null> =>
|
||||
client.page.evaluate(() => window.__store?.getState().activeContextualTourId ?? null),
|
||||
{ timeout: 60_000, message: 'intro tooltip never became the active contextual tour' }
|
||||
)
|
||||
.toBe('client-hosted-browser')
|
||||
|
||||
console.log(`\n=== TOOLTIP PREVIEW READY — window stays up ${HOLD_MINUTES} minutes ===\n`)
|
||||
await new Promise((resolve) => setTimeout(resolve, HOLD_MINUTES * 60_000))
|
||||
} finally {
|
||||
await dispose()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user