Files
orca/src/cli/browser.test.ts
T
Brennan BensonandOrca 1f43346c5f fix(resource-usage): hydrate pty-registry at boot; render · remote only for SSH repos (#1667)
* WIP: Changes before auto-review fixes

Co-authored-by: Orca <help@stably.ai>

* fix: address auto-review-fix-multi-agent findings

- Replace local ORCA_WORKTREE_ID_SEPARATOR with shared WORKTREE_ID_SEPARATOR
- Make hydrateLocalPtyRegistryAtBoot idempotent (one-shot per process,
  but stays retry-eligible until daemon provider is available)
- Strengthen daemon-pty-adapter strict-parser test to actually exercise
  the new short-circuit (test would have passed under the old loose
  parser too without the change)
- Add eslint-disable max-lines directive to oversized merge test file

Co-authored-by: Orca <help@stably.ai>

* chore: archive auto-review context to .context/

Co-authored-by: Orca <help@stably.ai>

* fix: address auto-review-fix findings

Drop the destructive reconcileOnStartup call from boot-time PTY registry
hydration: a transient listRepoWorktrees failure (returns [] and only
warns) would otherwise let the reconcile pass kill live local sessions.
The boot path is now read-only against the daemon — listSessions() only.

Also: tighten parsePtySessionId to reject degenerate `::` halves; replace
stale pty.ts:1005 references and a misleading local-unknown comment in
the hydrate module; narrow Store dependency to Pick<Store, 'getRepos'>;
log adapter listSessions failures instead of silently swallowing them;
re-anchor design-doc references on stable symbols and align §1b/§1c/§1d
with the implementation.

Co-authored-by: Orca <help@stably.ai>

* docs(resource-usage): update remote badge spec

Co-authored-by: Orca <help@stably.ai>

* test(resource-usage): cover boot hydration failure modes + warm-reattach e2e

Adds the regression coverage flagged in PR #1667's test plan that wasn't
already locked down.

vitest (`hydrate-local-pty-registry.test.ts`):
  - daemon offline at first call → no-op, hasHydrated stays false so a
    later macOS dock re-activation can retry.
  - listSessions rejection caught and logged, does not throw.
  - pid-write ordering: a pre-existing registry entry with pid=12345 is
    not clobbered by a stale `pid: null` from listSessions (§1d).
  - SSH-gate: a session whose repo has a non-null connectionId stays out
    of the registry, mirroring the spawn-time gate in pty.ts.
  - Happy-path: a local session is registered with the daemon's pid.

Playwright e2e (`resource-usage-warm-reattach.spec.ts`):
  Full quit→relaunch cycle against the same userDataDir; asserts that
  on the second launch the snapshot includes the warm-reattached PTY
  with a real pid before any pane mount, and that the seeded repo
  resolves as local (no connectionId). Mirrors the existing
  terminal-restart-persistence pattern.

Co-authored-by: Orca <help@stably.ai>

* fix(test): satisfy Pick<Store, 'getRepos'> in hydrator vitest

CI typecheck failed because FakeStore's getRepos returned objects missing
Repo's required fields (path, displayName, badgeColor, addedAt). Fill with
placeholder values; the hydrator only reads id + connectionId, but the
type signature still has to line up.

Co-authored-by: Orca <help@stably.ai>

* chore(resource-usage): drop bug-doc files; strip dead doc refs from comments

Remove docs/resource-usage-remote-mislabel.md (new in this PR) and revert
docs/resource-usage-merge-spec.md to the PR-base state. Strip the
matching `docs/...md §N` pointers from code/test comments, keeping the
surrounding "why" explanations intact so readers still get the
warm-reattach mislabel context.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-05-10 17:17:52 -07:00

633 lines
18 KiB
TypeScript

/* eslint-disable max-lines -- Why: this file groups every CLI browser-command test (page targeting, profiles, waits, viewport) so test-fixture imports and the runtime-client mock stay shared in one place. */
import path from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const callMock = vi.fn()
vi.mock('./runtime-client', () => {
class RuntimeClient {
call = callMock
getCliStatus = vi.fn()
openOrca = vi.fn()
}
class RuntimeClientError extends Error {
readonly code: string
constructor(code: string, message: string) {
super(message)
this.code = code
}
}
class RuntimeRpcFailureError extends RuntimeClientError {
readonly response: unknown
constructor(response: unknown) {
super('runtime_error', 'runtime_error')
this.response = response
}
}
return {
RuntimeClient,
RuntimeClientError,
RuntimeRpcFailureError
}
})
import { main } from './index'
import { RuntimeClientError } from './runtime-client'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
describe('orca cli browser page targeting', () => {
beforeEach(() => {
callMock.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('passes explicit page ids to snapshot without resolving the current worktree', async () => {
queueFixtures(
callMock,
okFixture('req_snapshot', {
browserPageId: 'page-1',
snapshot: 'tree',
refs: [],
url: 'https://example.com',
title: 'Example'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['snapshot', '--page', 'page-1', '--json'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.snapshot', { page: 'page-1' })
})
it('resolves current worktree only when --page is combined with --worktree current', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_snapshot', {
browserPageId: 'page-1',
snapshot: 'tree',
refs: [],
url: 'https://example.com',
title: 'Example'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['snapshot', '--page', 'page-1', '--worktree', 'current', '--json'],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 })
expect(callMock).toHaveBeenNthCalledWith(2, 'browser.snapshot', {
page: 'page-1',
worktree: `path:${path.resolve('/tmp/repo/feature')}`
})
})
it('passes page-targeted tab switches through without auto-scoping to the current worktree', async () => {
queueFixtures(callMock, okFixture('req_switch', { switched: 2, browserPageId: 'page-2' }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'switch', '--page', 'page-2', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabSwitch', {
index: undefined,
page: 'page-2'
})
})
it('still resolves the current worktree when tab switch --page is combined with --worktree current', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_switch', { switched: 2, browserPageId: 'page-2' })
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'switch', '--page', 'page-2', '--worktree', 'current', '--json'],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 })
expect(callMock).toHaveBeenNthCalledWith(2, 'browser.tabSwitch', {
index: undefined,
page: 'page-2',
worktree: `path:${path.resolve('/tmp/repo/feature')}`
})
})
it('passes focus: true through to browser.tabSwitch when --focus is set', async () => {
queueFixtures(callMock, okFixture('req_switch', { switched: 1, browserPageId: 'page-1' }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'switch', '--page', 'page-1', '--focus', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabSwitch', {
index: undefined,
page: 'page-1',
focus: true
})
})
it('omits focus from the payload when --focus is absent', async () => {
queueFixtures(callMock, okFixture('req_switch', { switched: 1, browserPageId: 'page-1' }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'switch', '--page', 'page-1', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenCalledTimes(1)
const call = callMock.mock.calls[0]
expect(call[0]).toBe('browser.tabSwitch')
expect(call[1]).not.toHaveProperty('focus')
})
it('passes explicit profile ids to tab create', async () => {
queueFixtures(callMock, okFixture('req_create', { browserPageId: 'page-3' }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'tab',
'create',
'--url',
'https://example.com',
'--profile',
'work',
'--worktree',
'all',
'--json'
],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith(
'browser.tabCreate',
{
url: 'https://example.com',
worktree: undefined,
profileId: 'work'
},
{ timeoutMs: 60_000 }
)
})
it('passes tab profile updates through by page id', async () => {
queueFixtures(
callMock,
okFixture('req_set_profile', {
browserPageId: 'page-2',
profileId: 'work',
profileLabel: 'Work'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'set', '--page', 'page-2', '--profile', 'work', '--json'],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabSetProfile', {
page: 'page-2',
profileId: 'work'
})
})
it('shows tab profile labels in text mode when requested', async () => {
queueFixtures(
callMock,
okFixture('req_list', {
tabs: [
{
browserPageId: 'page-1',
index: 0,
url: 'https://example.com',
title: 'Example',
active: true,
profileId: 'default',
profileLabel: 'Default'
},
{
browserPageId: 'page-2',
index: 1,
url: 'https://mail.example.com',
title: 'Mail',
active: false,
profileId: 'work',
profileLabel: 'Work'
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'list', '--show-profile', '--worktree', 'all'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabList', { worktree: undefined })
expect(logSpy).toHaveBeenCalledWith(
'* [0] page-1 Example — https://example.com [Default]\n' +
' [1] page-2 Mail — https://mail.example.com [Work]'
)
})
it('shows a single tab by page id', async () => {
queueFixtures(
callMock,
okFixture('req_tab_show', {
tab: {
browserPageId: 'page-1',
index: 0,
url: 'https://example.com',
title: 'Example',
active: true,
worktreeId: 'repo::/tmp/repo/feature',
profileId: 'work',
profileLabel: 'Work'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'show', '--page', 'page-1', '--json'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabShow', { page: 'page-1' })
})
it('resolves the current tab in a worktree', async () => {
queueFixtures(
callMock,
okFixture('req_tab_current', {
tab: {
browserPageId: 'page-2',
index: 1,
url: 'https://mail.example.com',
title: 'Mail',
active: true,
worktreeId: 'repo::/tmp/repo/feature',
profileId: 'default',
profileLabel: 'Default'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'current', '--worktree', 'all', '--json'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabCurrent', { worktree: undefined })
})
})
describe('orca cli browser tab profiles', () => {
beforeEach(() => {
callMock.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('shows the profile bound to a tab', async () => {
queueFixtures(
callMock,
okFixture('req_profile_show', {
browserPageId: 'page-2',
worktreeId: 'repo::/tmp/repo/feature',
profileId: 'work',
profileLabel: 'Work'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'show', '--page', 'page-2', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabProfileShow', { page: 'page-2' })
})
it('switches a tab back to the default profile', async () => {
queueFixtures(
callMock,
okFixture('req_profile_default', {
browserPageId: 'page-2',
profileId: 'default',
profileLabel: 'Default'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'use-default', '--page', 'page-2', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabSetProfile', {
page: 'page-2',
profileId: 'default'
})
})
it('clones a tab into a different profile', async () => {
queueFixtures(
callMock,
okFixture('req_profile_clone', {
browserPageId: 'page-9',
sourceBrowserPageId: 'page-2',
profileId: 'work',
profileLabel: 'Work'
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'clone', '--page', 'page-2', '--profile', 'work', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabProfileClone', {
page: 'page-2',
profileId: 'work'
})
})
})
describe('orca cli browser tab profiles', () => {
beforeEach(() => {
callMock.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('lists browser tab profiles', async () => {
queueFixtures(
callMock,
okFixture('req_profiles', {
profiles: [
{ id: 'default', scope: 'default', label: 'Default', partition: 'persist:orca-browser' },
{
id: 'work',
scope: 'isolated',
label: 'Work',
partition: 'persist:orca-browser-session-work'
}
]
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'profile', 'list', '--json'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.profileList')
})
it('reports an empty browser tab profile list with a friendly message', async () => {
queueFixtures(callMock, okFixture('req_profiles', { profiles: [] }))
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'profile', 'list'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.profileList')
expect(logSpy).toHaveBeenCalledWith('No browser profiles found.')
})
it('creates isolated browser tab profiles by default', async () => {
queueFixtures(
callMock,
okFixture('req_profile_create', {
profile: {
id: 'work',
scope: 'isolated',
label: 'Work',
partition: 'persist:orca-browser-session-work'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'create', '--label', 'Work', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.profileCreate', {
label: 'Work',
scope: 'isolated'
})
})
it('forwards --scope imported through to the runtime', async () => {
queueFixtures(
callMock,
okFixture('req_profile_create', {
profile: {
id: 'imp',
scope: 'imported',
label: 'From Chrome',
partition: 'persist:orca-browser-session-imp'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'create', '--label', 'From Chrome', '--scope', 'imported', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledWith('browser.profileCreate', {
label: 'From Chrome',
scope: 'imported'
})
})
it('rejects unknown --scope values instead of silently defaulting to isolated', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['tab', 'profile', 'create', '--label', 'Work', '--scope', 'isloated'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalledWith('--scope must be "isolated" or "imported"')
})
it('surfaces a runtime error if the registry refuses to create a profile', async () => {
queueFixtures(callMock, okFixture('req_profile_create', { profile: null }))
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['tab', 'profile', 'create', '--label', 'Bogus'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledTimes(1)
expect(errorSpy).toHaveBeenCalledWith(
'Failed to create browser profile (label=Bogus, scope=isolated)'
)
})
it('deletes browser tab profiles by id', async () => {
queueFixtures(callMock, okFixture('req_profile_delete', { deleted: true, profileId: 'work' }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['tab', 'profile', 'delete', '--profile', 'work', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.profileDelete', { profileId: 'work' })
})
it('reports a not-deleted profile in text mode without throwing', async () => {
queueFixtures(
callMock,
okFixture('req_profile_delete', { deleted: false, profileId: 'default' })
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'profile', 'delete', '--profile', 'default'], '/tmp/not-an-orca-worktree')
expect(callMock).toHaveBeenCalledWith('browser.profileDelete', { profileId: 'default' })
expect(logSpy).toHaveBeenCalledWith('Profile default was not deleted')
})
})
describe('orca cli browser waits and viewport flags', () => {
beforeEach(() => {
callMock.mockReset()
process.exitCode = undefined
})
afterEach(() => {
vi.restoreAllMocks()
})
it('gives selector waits an explicit RPC timeout budget', async () => {
queueFixtures(callMock, okFixture('req_wait', { ok: true }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['wait', '--selector', '#ready', '--worktree', 'all', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledWith(
'browser.wait',
{
selector: '#ready',
timeout: undefined,
text: undefined,
url: undefined,
load: undefined,
fn: undefined,
state: undefined,
worktree: undefined
},
{ timeoutMs: 60_000 }
)
})
it('extends selector wait RPC timeout when the user passes --timeout', async () => {
queueFixtures(callMock, okFixture('req_wait', { ok: true }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['wait', '--selector', '#ready', '--timeout', '12000', '--worktree', 'all', '--json'],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledWith(
'browser.wait',
{
selector: '#ready',
timeout: 12000,
text: undefined,
url: undefined,
load: undefined,
fn: undefined,
state: undefined,
worktree: undefined
},
{ timeoutMs: 17000 }
)
})
it('does not tell users Orca is down for a generic runtime timeout', async () => {
callMock.mockRejectedValueOnce(
new RuntimeClientError(
'runtime_timeout',
'Timed out waiting for the Orca runtime to respond.'
)
)
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['wait', '--selector', '#ready', '--worktree', 'all'], '/tmp/not-an-orca-worktree')
expect(errorSpy).toHaveBeenCalledWith('Timed out waiting for the Orca runtime to respond.')
})
it('passes the mobile viewport flag through to browser.viewport', async () => {
queueFixtures(
callMock,
okFixture('req_viewport', {
width: 375,
height: 812,
deviceScaleFactor: 2,
mobile: true
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'viewport',
'--width',
'375',
'--height',
'812',
'--scale',
'2',
'--mobile',
'--worktree',
'all',
'--json'
],
'/tmp/not-an-orca-worktree'
)
expect(callMock).toHaveBeenCalledWith('browser.viewport', {
width: 375,
height: 812,
deviceScaleFactor: 2,
mobile: true,
worktree: undefined
})
})
})