Files
orca/src/main/runtime/orca-runtime.test.ts
T
Jinwoo-HandOrca d751bd8a63 fix: repair test failures from mobile feature additions
- Add shouldRunSetupForCreate to hooks mock in orca-runtime tests
- Fix send() helper to use rest args, avoiding extra undefined in IPC calls

Co-authored-by: Orca <help@stably.ai>
2026-04-30 18:03:54 -04:00

2016 lines
62 KiB
TypeScript

/* eslint-disable max-lines -- Why: runtime behavior is stateful and cross-cutting, so these tests stay in one file to preserve the end-to-end invariants around handles, waits, and graph sync. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { WorktreeMeta } from '../../shared/types'
import { addWorktree, listWorktrees, removeWorktree } from '../git/worktree'
import {
createSetupRunnerScript,
getEffectiveHooks,
runHook,
shouldRunSetupForCreate
} from '../hooks'
import { OrchestrationDb } from './orchestration/db'
import { OrcaRuntimeService } from './orca-runtime'
const {
MOCK_GIT_WORKTREES,
addWorktreeMock,
removeWorktreeMock,
computeWorktreePathMock,
ensurePathWithinWorkspaceMock,
invalidateAuthorizedRootsCacheMock
} = vi.hoisted(() => ({
MOCK_GIT_WORKTREES: [
{
path: '/tmp/worktree-a',
head: 'abc',
branch: 'feature/foo',
isBare: false,
isMainWorktree: false
}
],
addWorktreeMock: vi.fn(),
removeWorktreeMock: vi.fn(),
computeWorktreePathMock: vi.fn(),
ensurePathWithinWorkspaceMock: vi.fn(),
invalidateAuthorizedRootsCacheMock: vi.fn()
}))
vi.mock('../git/worktree', () => ({
listWorktrees: vi.fn().mockResolvedValue(MOCK_GIT_WORKTREES),
addWorktree: addWorktreeMock,
removeWorktree: removeWorktreeMock
}))
vi.mock('../hooks', () => ({
createSetupRunnerScript: vi.fn(),
getEffectiveHooks: vi.fn().mockReturnValue(null),
runHook: vi.fn().mockResolvedValue({ success: true, output: '' }),
shouldRunSetupForCreate: vi.fn().mockReturnValue(false)
}))
vi.mock('../ipc/worktree-logic', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
computeWorktreePath: computeWorktreePathMock,
ensurePathWithinWorkspace: ensurePathWithinWorkspaceMock
}
})
vi.mock('../ipc/filesystem-auth', () => ({
invalidateAuthorizedRootsCache: invalidateAuthorizedRootsCacheMock
}))
// Why: the CLI create-worktree path calls getDefaultBaseRef to resolve a
// fallback base branch. Real resolution shells out to `git` against the
// test's fabricated repo path, which has no refs, so we stub it to a
// predictable 'origin/main'. The runtime no longer silently fabricates this
// default, so tests that want the legacy behavior must express it via the mock.
vi.mock('../git/repo', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
getDefaultBaseRef: vi.fn().mockReturnValue('origin/main'),
getBranchConflictKind: vi.fn().mockResolvedValue(null),
getGitUsername: vi.fn().mockReturnValue('')
}
})
afterEach(() => {
vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES)
vi.mocked(addWorktree).mockReset()
vi.mocked(removeWorktree).mockReset()
vi.mocked(createSetupRunnerScript).mockReset()
vi.mocked(getEffectiveHooks).mockReset()
vi.mocked(runHook).mockReset()
vi.mocked(getEffectiveHooks).mockReturnValue(null)
computeWorktreePathMock.mockReset()
ensurePathWithinWorkspaceMock.mockReset()
invalidateAuthorizedRootsCacheMock.mockReset()
})
function syncSinglePty(runtime: OrcaRuntimeService, ptyId: string | null = 'pty-1'): void {
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId,
paneTitle: null
}
]
})
}
const TEST_WINDOW_ID = 1
const TEST_REPO_ID = 'repo-1'
const TEST_REPO_PATH = '/tmp/repo'
const TEST_WORKTREE_PATH = '/tmp/worktree-a'
const TEST_WORKTREE_ID = `${TEST_REPO_ID}::${TEST_WORKTREE_PATH}`
function createRuntime(): OrcaRuntimeService {
return new OrcaRuntimeService(store)
}
const store = {
getRepo: (id: string) => store.getRepos().find((repo) => repo.id === id),
getRepos: () => [
{
id: TEST_REPO_ID,
path: TEST_REPO_PATH,
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1
}
],
addRepo: () => {},
updateRepo: (id: string, updates: Record<string, unknown>) =>
({
...store.getRepo(id),
...updates
}) as never,
getAllWorktreeMeta: () => ({
[TEST_WORKTREE_ID]: {
displayName: 'foo',
comment: '',
linkedIssue: 123,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
}),
getWorktreeMeta: (worktreeId: string) => store.getAllWorktreeMeta()[worktreeId],
getGitHubCache: () => ({ pr: {}, issue: {} }),
setWorktreeMeta: (_worktreeId: string, meta: Record<string, unknown>) =>
({
...store.getAllWorktreeMeta()[TEST_WORKTREE_ID],
...meta
}) as never,
removeWorktreeMeta: () => {},
getSettings: () => ({
workspaceDir: '/tmp/workspaces',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'none',
branchPrefixCustom: ''
})
}
computeWorktreePathMock.mockImplementation(
(
sanitizedName: string,
repoPath: string,
settings: { nestWorkspaces: boolean; workspaceDir: string }
) => {
if (settings.nestWorkspaces) {
const repoName =
repoPath
.split(/[\\/]/)
.at(-1)
?.replace(/\.git$/, '') ?? 'repo'
return `${settings.workspaceDir}/${repoName}/${sanitizedName}`
}
return `${settings.workspaceDir}/${sanitizedName}`
}
)
ensurePathWithinWorkspaceMock.mockImplementation((targetPath: string) => targetPath)
describe('OrcaRuntimeService', () => {
it('starts unavailable with no authoritative window', () => {
const runtime = createRuntime()
expect(runtime.getStatus()).toMatchObject({
graphStatus: 'unavailable',
authoritativeWindowId: null,
rendererGraphEpoch: 0
})
expect(runtime.getRuntimeId()).toBeTruthy()
})
it('claims the first window as authoritative and ignores later windows', () => {
const runtime = createRuntime()
runtime.attachWindow(TEST_WINDOW_ID)
runtime.attachWindow(2)
expect(runtime.getStatus().authoritativeWindowId).toBe(TEST_WINDOW_ID)
})
it('bumps the epoch and enters reloading when the authoritative window reloads', () => {
const runtime = createRuntime()
runtime.attachWindow(TEST_WINDOW_ID)
runtime.markGraphReady(TEST_WINDOW_ID)
runtime.markRendererReloading(TEST_WINDOW_ID)
expect(runtime.getStatus()).toMatchObject({
graphStatus: 'reloading',
rendererGraphEpoch: 1
})
})
it('can mark the graph ready for the authoritative window', () => {
const runtime = createRuntime()
runtime.attachWindow(TEST_WINDOW_ID)
runtime.markGraphReady(TEST_WINDOW_ID)
runtime.markRendererReloading(TEST_WINDOW_ID)
runtime.markGraphReady(TEST_WINDOW_ID)
expect(runtime.getStatus().graphStatus).toBe('ready')
})
it('drops back to unavailable and clears authority when the window disappears', () => {
const runtime = createRuntime()
runtime.attachWindow(TEST_WINDOW_ID)
runtime.markGraphReady(TEST_WINDOW_ID)
runtime.markRendererReloading(TEST_WINDOW_ID)
runtime.markGraphUnavailable(TEST_WINDOW_ID)
expect(runtime.getStatus()).toMatchObject({
graphStatus: 'unavailable',
authoritativeWindowId: null,
rendererGraphEpoch: 2
})
})
it('stays unavailable during initial loads before a graph is published', () => {
const runtime = createRuntime()
runtime.attachWindow(TEST_WINDOW_ID)
runtime.markRendererReloading(TEST_WINDOW_ID)
expect(runtime.getStatus()).toMatchObject({
graphStatus: 'unavailable',
rendererGraphEpoch: 0
})
})
it('lists live terminals and issues stable handles for synced leaves', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'hello from terminal\n', 123)
const terminals = await runtime.listTerminals('branch:feature/foo')
expect(terminals.terminals).toHaveLength(1)
expect(terminals.terminals[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
branch: 'feature/foo',
title: 'Claude',
preview: 'hello from terminal'
})
const shown = await runtime.showTerminal(terminals.terminals[0].handle)
expect(shown.handle).toBe(terminals.terminals[0].handle)
expect(shown.ptyId).toBe('pty-1')
})
it('resolves branch selectors when worktrees store refs/heads-prefixed branches', async () => {
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/worktree-a',
head: 'abc',
branch: 'refs/heads/Jinwoo-H/test-3a',
isBare: false,
isMainWorktree: false
}
])
const runtime = new OrcaRuntimeService(store)
const worktree = await runtime.showManagedWorktree('branch:Jinwoo-H/test-3a')
expect(worktree).toMatchObject({
branch: 'refs/heads/Jinwoo-H/test-3a',
path: '/tmp/worktree-a'
})
})
it('does not interpret active as a runtime-global worktree selector', async () => {
const runtime = new OrcaRuntimeService(store)
await expect(runtime.showManagedWorktree('active')).rejects.toThrow('selector_not_found')
})
it('reads bounded terminal output and writes through the PTY controller', async () => {
const writes: string[] = []
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: (_ptyId, data) => {
writes.push(data)
return true
},
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', '\u001b[32mhello\u001b[0m\nworld\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
const read = await runtime.readTerminal(terminal.handle)
expect(read).toMatchObject({
handle: terminal.handle,
status: 'running',
tail: ['hello', 'world'],
truncated: false,
nextCursor: expect.any(String)
})
const send = await runtime.sendTerminal(terminal.handle, {
text: 'continue',
enter: true
})
expect(send).toMatchObject({
handle: terminal.handle,
accepted: true
})
expect(writes).toEqual(['continue', '\r'])
})
it('serializes terminal state from runtime-tracked PTY output when renderer has no buffer', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
getSize: () => ({ cols: 100, rows: 30 }),
serializeBuffer: vi.fn().mockResolvedValue(null)
})
runtime.onPtyData('pty-1', '\x1b[31mred text\x1b[0m\n', 123)
const serialized = await runtime.serializeTerminalBuffer('pty-1')
expect(serialized).toMatchObject({ cols: 100, rows: 30 })
expect(serialized?.data).toContain('red text')
expect(serialized?.data).toContain('[31m')
})
it('prefers runtime-tracked screen snapshots over renderer scrollback snapshots', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
getSize: () => ({ cols: 100, rows: 30 }),
serializeBuffer: vi.fn().mockResolvedValue({
data: 'renderer scrollback that should not win',
cols: 100,
rows: 30
})
})
runtime.onPtyData('pty-1', '\x1b[32mruntime screen\x1b[0m\n', 123)
const serialized = await runtime.serializeTerminalBuffer('pty-1')
expect(serialized?.data).toContain('runtime screen')
expect(serialized?.data).not.toContain('renderer scrollback')
})
it('does not list renderer placeholder leaves alongside live PTY fallback terminals', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: vi.fn().mockResolvedValue([
{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'setup' },
{ id: 'pty-2', cwd: '/tmp/worktree-a', title: 'Claude Code' }
])
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-setup',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'setup',
activeLeafId: 'pane:1',
layout: null
},
{
tabId: 'tab-claude',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude Code',
activeLeafId: 'pane:2',
layout: null
}
],
leaves: [
{
tabId: 'tab-setup',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: null
},
{
tabId: 'tab-claude',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:2',
paneRuntimeId: 2,
ptyId: null
}
]
})
const terminals = await runtime.listTerminals('branch:feature/foo')
expect(terminals.terminals).toHaveLength(2)
expect(terminals.terminals.every((terminal) => terminal.connected)).toBe(true)
expect(new Set(terminals.terminals.map((terminal) => terminal.handle)).size).toBe(2)
})
it('waits for terminal exit and resolves with the exit status', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
const waitPromise = runtime.waitForTerminal(terminal.handle, { timeoutMs: 1000 })
runtime.onPtyExit('pty-1', 7)
await expect(waitPromise).resolves.toMatchObject({
handle: terminal.handle,
condition: 'exit',
satisfied: true,
status: 'exited',
exitCode: 7
})
})
it('keeps partial-line output readable across cursor-based pagination', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'hel', 100)
// Non-cursor reads include the partial line for UI display
const firstRead = await runtime.readTerminal(terminal.handle)
expect(firstRead.tail).toEqual(['hel'])
expect(firstRead.nextCursor).toBe('0')
runtime.onPtyData('pty-1', 'lo', 101)
// Cursor-based reads exclude partial lines to prevent duplication:
// without this, the consumer would see "hello" now as a partial, then
// see "hello" again as a completed line on the next read.
const secondRead = await runtime.readTerminal(terminal.handle, {
cursor: Number(firstRead.nextCursor)
})
expect(secondRead.tail).toEqual([])
expect(secondRead.nextCursor).toBe('0')
runtime.onPtyData('pty-1', '\nworld\n', 102)
const thirdRead = await runtime.readTerminal(terminal.handle, {
cursor: Number(secondRead.nextCursor)
})
expect(thirdRead.tail).toEqual(['hello', 'world'])
expect(thirdRead.nextCursor).toBe('2')
})
it('delivers pending orchestration messages to an already-idle agent', async () => {
vi.useFakeTimers()
const runtime = new OrcaRuntimeService(store)
const db = new OrchestrationDb(':memory:')
const write = vi.fn().mockReturnValue(true)
runtime.setOrchestrationDb(db)
runtime.setPtyController({
write,
kill: vi.fn(),
getForegroundProcess: async () => null
})
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100)
runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07', 101)
db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello' })
runtime.deliverPendingMessagesForHandle(terminal.handle)
expect(write).toHaveBeenCalledWith('pty-1', expect.stringContaining('Subject: hello'))
// Why: markAsRead is deferred until the 500ms delayed Enter is confirmed,
// so we must advance timers past the split-write delay.
await vi.advanceTimersByTimeAsync(500)
expect(write).toHaveBeenCalledWith('pty-1', '\r')
expect(db.getUnreadMessages(terminal.handle)).toHaveLength(0)
db.close()
vi.useRealTimers()
})
it('adopts preallocated ORCA_TERMINAL_HANDLE as a valid runtime handle', async () => {
const runtime = new OrcaRuntimeService(store)
const handle = runtime.preAllocateHandleForPty('pty-1')
syncSinglePty(runtime)
runtime.onPtyData('pty-1', 'ready\n', 100)
const read = await runtime.readTerminal(handle)
expect(read.handle).toBe(handle)
expect(read.tail).toEqual(['ready'])
})
it('keeps preallocated terminal handles valid across renderer reloads', async () => {
const runtime = new OrcaRuntimeService(store)
const handle = runtime.preAllocateHandleForPty('pty-1')
syncSinglePty(runtime)
runtime.markRendererReloading(1)
syncSinglePty(runtime, null)
runtime.onPtyData('pty-1', 'after reload\n', 100)
const read = await runtime.readTerminal(handle)
expect(read.tail).toEqual(['after reload'])
})
it('keeps preallocated terminal handles valid when a reload graph omits the live leaf', async () => {
const runtime = new OrcaRuntimeService(store)
const handle = runtime.preAllocateHandleForPty('pty-1')
syncSinglePty(runtime)
runtime.markRendererReloading(1)
runtime.syncWindowGraph(1, {
tabs: [],
leaves: []
})
runtime.onPtyData('pty-1', 'after omitted leaf\n', 100)
const read = await runtime.readTerminal(handle)
expect(read.tail).toEqual(['after omitted leaf'])
})
it('keeps preallocated terminal handles valid after graph unavailable during reload', async () => {
const runtime = new OrcaRuntimeService(store)
const handle = runtime.preAllocateHandleForPty('pty-1')
syncSinglePty(runtime)
runtime.markGraphUnavailable(1)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [],
leaves: []
})
runtime.onPtyData('pty-1', 'after unavailable\n', 100)
const read = await runtime.readTerminal(handle)
expect(read.tail).toEqual(['after unavailable'])
})
it('keeps already-idle status after tui-idle wait for immediate message delivery', async () => {
const runtime = new OrcaRuntimeService(store)
const db = new OrchestrationDb(':memory:')
const write = vi.fn().mockReturnValue(true)
runtime.setOrchestrationDb(db)
runtime.setPtyController({
write,
kill: vi.fn(),
getForegroundProcess: async () => null
})
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100)
runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07', 101)
await runtime.waitForTerminal(terminal.handle, { condition: 'tui-idle' })
db.insertMessage({ from: 'sender', to: terminal.handle, subject: 'after wait' })
runtime.deliverPendingMessagesForHandle(terminal.handle)
expect(write).toHaveBeenCalledWith('pty-1', expect.stringContaining('Subject: after wait'))
db.close()
})
it('resolves message waiters when notifyMessageArrived is called', async () => {
const runtime = new OrcaRuntimeService(store)
const waitPromise = runtime.waitForMessage('term_abc', { timeoutMs: 5000 })
runtime.notifyMessageArrived('term_abc')
await waitPromise
})
it('resolves message waiters on timeout when no message arrives', async () => {
const runtime = new OrcaRuntimeService(store)
const start = Date.now()
await runtime.waitForMessage('term_abc', { timeoutMs: 100 })
const elapsed = Date.now() - start
expect(elapsed).toBeGreaterThanOrEqual(90)
expect(elapsed).toBeLessThan(500)
})
it('fails terminal waits closed when the handle goes stale during reload', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
const waitPromise = runtime.waitForTerminal(terminal.handle, { timeoutMs: 1000 })
runtime.markRendererReloading(1)
await expect(waitPromise).rejects.toThrow('terminal_handle_stale')
})
it('tui-idle times out when PTY data has no agent OSC title transitions', async () => {
vi.useFakeTimers()
try {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Terminal 1',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'running migration step 4/9\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
const waitPromise = runtime.waitForTerminal(terminal.handle, {
condition: 'tui-idle',
timeoutMs: 1_000
})
const timeoutAssertion = expect(waitPromise).rejects.toThrow('timeout')
await vi.advanceTimersByTimeAsync(12_000)
await timeoutAssertion
} finally {
vi.useRealTimers()
}
})
it('tui-idle resolves on agent working→idle OSC title transition', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
// Simulate agent starting work (braille spinner = working)
runtime.onPtyData('pty-1', '\x1b]0;\u280b Working on task\x07output\n', 100)
const [terminal] = (await runtime.listTerminals()).terminals
const waitPromise = runtime.waitForTerminal(terminal.handle, {
condition: 'tui-idle',
timeoutMs: 5_000
})
// Simulate agent finishing (✳ = Claude Code idle)
runtime.onPtyData('pty-1', '\x1b]0;\u2733 Task complete\x07done\n', 200)
const result = await waitPromise
expect(result.condition).toBe('tui-idle')
expect(result.satisfied).toBe(true)
})
it('builds a compact worktree summary from persisted and live runtime state', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'build green\n', 321)
const summaries = await runtime.getWorktreePs()
expect(summaries).toMatchObject({
worktrees: [
{
worktreeId: 'repo-1::/tmp/worktree-a',
repoId: 'repo-1',
repo: 'repo',
path: '/tmp/worktree-a',
branch: 'feature/foo',
linkedIssue: 123,
unread: false,
liveTerminalCount: 1,
hasAttachedPty: true,
lastOutputAt: 321,
preview: 'build green'
}
],
totalCount: 1,
truncated: false
})
})
it('matches live terminal summaries when renderer worktree paths are equivalent but not identical', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/child/../worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/child/../worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'path-normalized\n', 456)
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
hasAttachedPty: true,
lastOutputAt: 456,
preview: 'path-normalized'
})
})
it('does not classify ordinary terminal output as working in worktree ps', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'zsh',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1',
title: 'zsh'
}
]
})
runtime.onPtyData('pty-1', 'README.md\npackage.json\n', 999)
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
lastOutputAt: 999,
status: 'active'
})
})
it('classifies agent title state in worktree ps', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Codex working',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1',
title: 'Codex working'
}
]
})
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
status: 'working'
})
})
it('keeps mobile worktree ps populated when the renderer leaf graph is empty', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
runtime.onPtyData('pty-1', 'still alive\n', 654)
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
hasAttachedPty: true,
lastOutputAt: 654,
preview: 'still alive'
})
})
it('counts a main-process PTY registration before any renderer leaf is synced', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', 'repo-1::/tmp/worktree-a')
runtime.onPtyData('pty-1', 'registered first\n', 777)
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
hasAttachedPty: true,
lastOutputAt: 777,
preview: 'registered first'
})
})
it('discovers existing daemon PTYs from the controller process list', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [
{
id: 'repo-1::/tmp/worktree-a@@abc12345',
cwd: '/tmp/worktree-a',
title: 'bash'
}
]
})
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 1,
hasAttachedPty: true
})
})
it('lists and controls daemon PTYs when the renderer graph has no leaves', async () => {
const runtime = new OrcaRuntimeService(store)
const writes: string[] = []
runtime.setPtyController({
write: (_ptyId, data) => {
writes.push(data)
return true
},
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [
{
id: 'pty-orphan',
cwd: TEST_WORKTREE_PATH,
title: 'bash'
}
]
})
runtime.attachWindow(TEST_WINDOW_ID)
runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [], leaves: [] })
const list = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
expect(list).toMatchObject({
totalCount: 1,
truncated: false
})
expect(list.terminals[0]).toMatchObject({
worktreeId: TEST_WORKTREE_ID,
connected: true,
writable: true,
preview: ''
})
const handle = list.terminals[0]!.handle
runtime.onPtyData('pty-orphan', 'ready\n', 456)
await expect(runtime.readTerminal(handle)).resolves.toMatchObject({
handle,
status: 'running',
tail: ['ready'],
truncated: false
})
await expect(
runtime.sendTerminal(handle, {
text: 'pwd',
enter: true
})
).resolves.toMatchObject({
handle,
accepted: true
})
expect(writes).toEqual(['pwd\r'])
})
it('counts persisted desktop terminal tabs when no live leaf is mounted', async () => {
const runtime = new OrcaRuntimeService({
...store,
getWorkspaceSession: () => ({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
terminalLayoutsByTabId: {},
tabsByWorktree: {
'repo-1::/tmp/worktree-a': [
{
id: 'tab-1',
ptyId: null,
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
},
{
id: 'tab-2',
ptyId: 'repo-1::/tmp/worktree-a@@abc12345',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 2
}
]
}
})
})
const summaries = await runtime.getWorktreePs()
expect(summaries.worktrees[0]).toMatchObject({
worktreeId: 'repo-1::/tmp/worktree-a',
liveTerminalCount: 2,
hasAttachedPty: true
})
})
it('fails terminal stop closed while the renderer graph is reloading', async () => {
const runtime = new OrcaRuntimeService(store)
let killed = false
runtime.setPtyController({
write: () => true,
kill: () => {
killed = true
return true
},
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.markRendererReloading(1)
await expect(runtime.stopTerminalsForWorktree('id:repo-1::/tmp/worktree-a')).rejects.toThrow(
'runtime_unavailable'
)
expect(killed).toBe(false)
})
it('fails terminal listing closed if the graph reloads during selector resolution', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
let releaseListWorktrees = () => {}
vi.mocked(listWorktrees).mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseListWorktrees = () => resolve(MOCK_GIT_WORKTREES)
})
)
const listPromise = runtime.listTerminals('branch:feature/foo')
runtime.markRendererReloading(1)
releaseListWorktrees()
await expect(listPromise).rejects.toThrow('runtime_unavailable')
})
it('fails terminal stop closed if the graph reloads during selector resolution', async () => {
const runtime = new OrcaRuntimeService(store)
let killed = false
runtime.setPtyController({
write: () => true,
kill: () => {
killed = true
return true
},
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
let releaseListWorktrees = () => {}
vi.mocked(listWorktrees).mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseListWorktrees = () => resolve(MOCK_GIT_WORKTREES)
})
)
const stopPromise = runtime.stopTerminalsForWorktree('branch:feature/foo')
runtime.markRendererReloading(1)
releaseListWorktrees()
await expect(stopPromise).rejects.toThrow('runtime_unavailable')
expect(killed).toBe(false)
})
it('rejects invalid positive limits for bounded list commands', async () => {
const runtime = new OrcaRuntimeService(store)
await expect(runtime.getWorktreePs(-1)).rejects.toThrow('invalid_limit')
await expect(runtime.listManagedWorktrees(undefined, 0)).rejects.toThrow('invalid_limit')
await expect(runtime.searchRepoRefs('id:repo-1', 'main', -5)).rejects.toThrow('invalid_limit')
})
it('activates an existing worktree through the renderer notifier', async () => {
const runtime = new OrcaRuntimeService(store)
const activateWorktree = vi.fn()
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
runtime.attachWindow(TEST_WINDOW_ID)
runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [], leaves: [] })
await expect(runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toEqual({
repoId: TEST_REPO_ID,
worktreeId: TEST_WORKTREE_ID,
activated: true
})
expect(activateWorktree).toHaveBeenCalledWith(TEST_REPO_ID, TEST_WORKTREE_ID)
})
it('returns a setup launch payload for CLI-created worktrees when hooks are explicitly enabled', async () => {
const runtime = new OrcaRuntimeService(store)
const activateWorktree = vi.fn()
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
runtime.attachWindow(1)
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-hook-test')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-hook-test')
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
vi.mocked(getEffectiveHooks).mockReturnValue({
scripts: {
setup: 'pnpm worktree:setup'
}
})
vi.mocked(createSetupRunnerScript).mockReturnValue({
runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
envVars: {
ORCA_ROOT_PATH: '/tmp/repo',
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-hook-test'
}
})
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/runtime-hook-test',
head: 'def',
branch: 'runtime-hook-test',
isBare: false,
isMainWorktree: false
}
])
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-hook-test',
runHooks: true
})
expect(createSetupRunnerScript).toHaveBeenCalledWith(
expect.objectContaining({ id: 'repo-1', path: '/tmp/repo' }),
'/tmp/workspaces/runtime-hook-test',
'pnpm worktree:setup'
)
expect(runHook).not.toHaveBeenCalled()
expect(addWorktree).toHaveBeenCalledWith(
'/tmp/repo',
'/tmp/workspaces/runtime-hook-test',
'runtime-hook-test',
'origin/main',
false
)
expect(result).toEqual({
worktree: expect.objectContaining({
repoId: 'repo-1',
path: '/tmp/workspaces/runtime-hook-test',
branch: 'runtime-hook-test'
}),
setup: {
runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
envVars: {
ORCA_ROOT_PATH: '/tmp/repo',
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-hook-test'
}
}
})
expect(activateWorktree).toHaveBeenCalledWith('repo-1', expect.any(String), result.setup)
})
it('skips setup hooks for CLI-created worktrees by default', async () => {
const runtime = new OrcaRuntimeService(store)
const activateWorktree = vi.fn()
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn()
})
runtime.attachWindow(1)
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-hook-skip')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-hook-skip')
vi.mocked(getEffectiveHooks).mockReturnValue({
scripts: {
setup: 'pnpm worktree:setup'
}
})
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/runtime-hook-skip',
head: 'def',
branch: 'runtime-hook-skip',
isBare: false,
isMainWorktree: false
}
])
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-hook-skip'
})
expect(createSetupRunnerScript).not.toHaveBeenCalled()
expect(runHook).not.toHaveBeenCalled()
expect(result).toEqual({
worktree: expect.objectContaining({
repoId: 'repo-1',
path: '/tmp/workspaces/runtime-hook-skip',
branch: 'runtime-hook-skip'
}),
warning:
'orca.yaml setup hook skipped for /tmp/workspaces/runtime-hook-skip; pass --run-hooks to run it.'
})
expect(activateWorktree).toHaveBeenCalledWith('repo-1', expect.any(String), undefined)
})
it('skips archive hooks for CLI worktree removal by default', async () => {
const runtime = new OrcaRuntimeService(store)
vi.mocked(getEffectiveHooks).mockReturnValue({
scripts: {
archive: 'pnpm worktree:archive'
}
})
vi.mocked(removeWorktree).mockResolvedValue(undefined)
const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID)
expect(runHook).not.toHaveBeenCalled()
expect(removeWorktree).toHaveBeenCalledWith(TEST_REPO_PATH, TEST_WORKTREE_PATH, false)
expect(result.warning).toBe(
`orca.yaml archive hook skipped for ${TEST_WORKTREE_PATH}; pass --run-hooks to run it.`
)
})
it('runs archive hooks for CLI worktree removal when hooks are explicitly enabled', async () => {
const runtime = new OrcaRuntimeService(store)
vi.mocked(getEffectiveHooks).mockReturnValue({
scripts: {
archive: 'pnpm worktree:archive'
}
})
vi.mocked(runHook).mockResolvedValue({ success: true, output: '' })
vi.mocked(removeWorktree).mockResolvedValue(undefined)
await runtime.removeManagedWorktree(TEST_WORKTREE_ID, false, true)
expect(runHook).toHaveBeenCalledWith(
'archive',
TEST_WORKTREE_PATH,
expect.objectContaining({ id: TEST_REPO_ID, path: TEST_REPO_PATH })
)
expect(removeWorktree).toHaveBeenCalledWith(TEST_REPO_PATH, TEST_WORKTREE_PATH, false)
})
it('invalidates the filesystem-auth cache after CLI worktree creation', async () => {
// Reproduces: CLI-created worktrees fail with "Access denied: unknown
// repository or worktree path" because the filesystem-auth cache was
// not invalidated, so git:branchCompare could not resolve the new path.
const runtime = new OrcaRuntimeService(store)
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/cli-worktree')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/cli-worktree')
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/cli-worktree',
head: 'abc',
branch: 'cli-worktree',
isBare: false,
isMainWorktree: false
}
])
await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'cli-worktree'
})
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled()
})
it('preserves create-time metadata on later runtime listings when Windows path formatting differs', async () => {
const metaById: Record<string, WorktreeMeta> = {}
const runtimeStore = {
getRepo: (id: string) => runtimeStore.getRepos().find((repo) => repo.id === id),
getRepos: () => [
{
id: 'repo-1',
path: 'C:\\repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1
}
],
addRepo: () => {},
updateRepo: () => undefined as never,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
const existingMeta = metaById[worktreeId]
const nextMeta: WorktreeMeta = {
displayName: meta.displayName ?? existingMeta?.displayName ?? '',
comment: meta.comment ?? existingMeta?.comment ?? '',
linkedIssue: meta.linkedIssue ?? existingMeta?.linkedIssue ?? null,
linkedPR: meta.linkedPR ?? existingMeta?.linkedPR ?? null,
linkedLinearIssue: meta.linkedLinearIssue ?? existingMeta?.linkedLinearIssue ?? null,
isArchived: meta.isArchived ?? existingMeta?.isArchived ?? false,
isUnread: meta.isUnread ?? existingMeta?.isUnread ?? false,
isPinned: meta.isPinned ?? existingMeta?.isPinned ?? false,
sortOrder: meta.sortOrder ?? existingMeta?.sortOrder ?? 0,
lastActivityAt: meta.lastActivityAt ?? existingMeta?.lastActivityAt ?? 0
}
metaById[worktreeId] = nextMeta
return nextMeta
},
removeWorktreeMeta: () => {},
getGitHubCache: () => ({ pr: {}, issue: {} }),
getSettings: () => ({
workspaceDir: 'C:\\workspaces',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'none',
branchPrefixCustom: ''
})
}
computeWorktreePathMock.mockReturnValue('C:\\workspaces\\improve-dashboard')
ensurePathWithinWorkspaceMock.mockReturnValue('C:\\workspaces\\improve-dashboard')
vi.mocked(listWorktrees)
.mockResolvedValueOnce([
{
path: 'C:/workspaces/improve-dashboard',
head: 'abc',
branch: 'refs/heads/improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
.mockResolvedValueOnce([
{
path: 'C:/workspaces/improve-dashboard',
head: 'abc',
branch: 'refs/heads/improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
const runtime = new OrcaRuntimeService(runtimeStore)
await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'Improve Dashboard'
})
const listed = await runtime.listManagedWorktrees('id:repo-1')
expect(listed.worktrees).toMatchObject([
{
id: 'repo-1::C:/workspaces/improve-dashboard',
displayName: 'Improve Dashboard'
}
])
})
describe('terminal fit overrides', () => {
function createRuntimeWithResize() {
const runtime = new OrcaRuntimeService(store)
const resizes: { ptyId: string; cols: number; rows: number }[] = []
const notifications: {
ptyId: string
mode: string
cols: number
rows: number
}[] = []
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
resize: (ptyId, cols, rows) => {
resizes.push({ ptyId, cols, rows })
return true
},
getSize: () => ({ cols: 150, rows: 40 })
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: (ptyId, mode, cols, rows) => {
notifications.push({ ptyId, mode, cols, rows })
}
})
return { runtime, resizes, notifications }
}
it('clamps dimensions within valid range', () => {
const { runtime, resizes } = createRuntimeWithResize()
const result = runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 10, 5)
expect(result.cols).toBe(20)
expect(result.rows).toBe(8)
expect(resizes[0]).toEqual({ ptyId: 'pty-1', cols: 20, rows: 8 })
})
it('clamps large dimensions to upper bounds', () => {
const { runtime } = createRuntimeWithResize()
const result = runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 500, 300)
expect(result.cols).toBe(240)
expect(result.rows).toBe(120)
})
it('rejects missing dimensions for mobile-fit', () => {
const { runtime } = createRuntimeWithResize()
expect(() => runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a')).toThrow(
'invalid_dimensions'
)
})
it('rejects NaN dimensions', () => {
const { runtime } = createRuntimeWithResize()
expect(() => runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', NaN, 24)).toThrow(
'invalid_dimensions'
)
})
it('preserves original previousSize across re-fits', () => {
const { runtime } = createRuntimeWithResize()
const first = runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
expect(first.previousCols).toBe(150)
expect(first.previousRows).toBe(40)
const second = runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 60, 20)
expect(second.previousCols).toBe(150)
expect(second.previousRows).toBe(40)
})
it('restores for the owning client and resizes PTY back', () => {
const { runtime, resizes, notifications } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
const result = runtime.resizeForClient('pty-1', 'restore', 'client-a')
expect(result.mode).toBe('desktop-fit')
expect(result.cols).toBe(150)
expect(result.rows).toBe(40)
expect(runtime.getTerminalFitOverride('pty-1')).toBeNull()
expect(notifications).toHaveLength(2)
expect(notifications[1]).toMatchObject({ ptyId: 'pty-1', mode: 'desktop-fit' })
expect(resizes).toEqual([
{ ptyId: 'pty-1', cols: 80, rows: 24 },
{ ptyId: 'pty-1', cols: 150, rows: 40 }
])
})
it('rejects restore from non-owning client', () => {
const { runtime } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
expect(() => runtime.resizeForClient('pty-1', 'restore', 'client-b')).toThrow(
'not_override_owner'
)
})
it('rejects restore when no override exists', () => {
const { runtime } = createRuntimeWithResize()
expect(() => runtime.resizeForClient('pty-1', 'restore', 'client-a')).toThrow(
'no_active_override'
)
})
it('allows a second client to overwrite the first (latest-writer-wins)', () => {
const { runtime } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-b', 60, 20)
expect(() => runtime.resizeForClient('pty-1', 'restore', 'client-a')).toThrow(
'not_override_owner'
)
const result = runtime.resizeForClient('pty-1', 'restore', 'client-b')
expect(result.mode).toBe('desktop-fit')
})
it('auto-restores on client disconnect', () => {
const { runtime } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
runtime.resizeForClient('pty-2', 'mobile-fit', 'client-a', 60, 20)
runtime.resizeForClient('pty-3', 'mobile-fit', 'client-b', 40, 15)
runtime.onClientDisconnected('client-a')
expect(runtime.getTerminalFitOverride('pty-1')).toBeNull()
expect(runtime.getTerminalFitOverride('pty-2')).toBeNull()
expect(runtime.getTerminalFitOverride('pty-3')).not.toBeNull()
})
it('clears override on PTY exit', () => {
const { runtime, notifications } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
runtime.onPtyExit('pty-1', 0)
expect(runtime.getTerminalFitOverride('pty-1')).toBeNull()
const exitNotification = notifications.find(
(n) => n.ptyId === 'pty-1' && n.mode === 'desktop-fit'
)
expect(exitNotification).toBeTruthy()
})
it('returns all active overrides via getAllTerminalFitOverrides', () => {
const { runtime } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
runtime.resizeForClient('pty-2', 'mobile-fit', 'client-b', 60, 20)
const all = runtime.getAllTerminalFitOverrides()
expect(all.size).toBe(2)
expect(all.get('pty-1')).toEqual({ mode: 'mobile-fit', cols: 80, rows: 24 })
expect(all.get('pty-2')).toEqual({ mode: 'mobile-fit', cols: 60, rows: 20 })
})
it('rolls back override if resize fails', () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
resize: () => false
})
expect(() => runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)).toThrow(
'resize_failed'
)
expect(runtime.getTerminalFitOverride('pty-1')).toBeNull()
})
it('notifies renderer on mobile-fit', () => {
const { runtime, notifications } = createRuntimeWithResize()
runtime.resizeForClient('pty-1', 'mobile-fit', 'client-a', 80, 24)
expect(notifications).toHaveLength(1)
expect(notifications[0]).toEqual({
ptyId: 'pty-1',
mode: 'mobile-fit',
cols: 80,
rows: 24
})
})
})
describe('browser page targeting', () => {
it('passes explicit page ids through without resolving the current worktree', async () => {
vi.mocked(listWorktrees).mockClear()
const runtime = createRuntime()
const snapshotMock = vi.fn().mockResolvedValue({
browserPageId: 'page-1',
snapshot: 'tree',
refs: [],
url: 'https://example.com',
title: 'Example'
})
runtime.setAgentBrowserBridge({
snapshot: snapshotMock
} as never)
const result = await runtime.browserSnapshot({ page: 'page-1' })
expect(result.browserPageId).toBe('page-1')
expect(snapshotMock).toHaveBeenCalledWith(undefined, 'page-1')
expect(listWorktrees).not.toHaveBeenCalled()
})
it('resolves explicit worktree selectors when page ids are also provided', async () => {
vi.mocked(listWorktrees).mockClear()
const runtime = createRuntime()
const snapshotMock = vi.fn().mockResolvedValue({
browserPageId: 'page-1',
snapshot: 'tree',
refs: [],
url: 'https://example.com',
title: 'Example'
})
runtime.setAgentBrowserBridge({
snapshot: snapshotMock,
getRegisteredTabs: vi.fn(() => new Map([['page-1', 1]]))
} as never)
await runtime.browserSnapshot({
worktree: 'branch:feature/foo',
page: 'page-1'
})
expect(snapshotMock).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'page-1')
})
it('routes tab switch and capture start by explicit page id', async () => {
const runtime = createRuntime()
const tabSwitchMock = vi.fn().mockResolvedValue({
switched: 2,
browserPageId: 'page-2'
})
const captureStartMock = vi.fn().mockResolvedValue({
capturing: true
})
runtime.setAgentBrowserBridge({
tabSwitch: tabSwitchMock,
captureStart: captureStartMock
} as never)
await expect(runtime.browserTabSwitch({ page: 'page-2' })).resolves.toEqual({
switched: 2,
browserPageId: 'page-2'
})
await expect(runtime.browserCaptureStart({ page: 'page-2' })).resolves.toEqual({
capturing: true
})
expect(tabSwitchMock).toHaveBeenCalledWith(undefined, undefined, 'page-2')
expect(captureStartMock).toHaveBeenCalledWith(undefined, 'page-2')
})
it('does not silently drop invalid explicit worktree selectors for page-targeted commands', async () => {
vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES)
const runtime = createRuntime()
const snapshotMock = vi.fn()
runtime.setAgentBrowserBridge({
snapshot: snapshotMock,
getRegisteredTabs: vi.fn(() => new Map([['page-1', 1]]))
} as never)
await expect(
runtime.browserSnapshot({
worktree: 'path:/tmp/missing-worktree',
page: 'page-1'
})
).rejects.toThrow('selector_not_found')
expect(snapshotMock).not.toHaveBeenCalled()
})
it('does not silently drop invalid explicit worktree selectors for non-page browser commands', async () => {
vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES)
const runtime = createRuntime()
const tabListMock = vi.fn()
runtime.setAgentBrowserBridge({
tabList: tabListMock
} as never)
await expect(
runtime.browserTabList({
worktree: 'path:/tmp/missing-worktree'
})
).rejects.toThrow('selector_not_found')
expect(tabListMock).not.toHaveBeenCalled()
})
it('rejects closing an unknown page id instead of treating it as success', async () => {
vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES)
const runtime = createRuntime()
runtime.setAgentBrowserBridge({
getRegisteredTabs: vi.fn(() => new Map([['page-1', 1]]))
} as never)
await expect(
runtime.browserTabClose({
page: 'missing-page'
})
).rejects.toThrow('Browser page missing-page was not found')
})
it('rejects closing a page outside the explicitly scoped worktree', async () => {
vi.mocked(listWorktrees).mockResolvedValue([
...MOCK_GIT_WORKTREES,
{
path: '/tmp/worktree-b',
head: 'def',
branch: 'feature/bar',
isBare: false,
isMainWorktree: false
}
])
const runtime = createRuntime()
const getRegisteredTabsMock = vi.fn((worktreeId?: string) =>
worktreeId === `${TEST_REPO_ID}::/tmp/worktree-b` ? new Map() : new Map([['page-1', 1]])
)
runtime.setAgentBrowserBridge({
getRegisteredTabs: getRegisteredTabsMock
} as never)
await expect(
runtime.browserTabClose({
page: 'page-1',
worktree: 'path:/tmp/worktree-b'
})
).rejects.toThrow('Browser page page-1 was not found in this worktree')
expect(getRegisteredTabsMock).toHaveBeenCalledWith(`${TEST_REPO_ID}::/tmp/worktree-b`)
})
})
describe('subscribeToTerminalData', () => {
it('delivers PTY data to a subscriber', () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
const chunks: string[] = []
runtime.subscribeToTerminalData('pty-1', (data) => chunks.push(data))
runtime.onPtyData('pty-1', 'hello\n', 100)
runtime.onPtyData('pty-1', 'world\n', 101)
expect(chunks).toEqual(['hello\n', 'world\n'])
})
it('unsubscribe removes the listener so data is not delivered twice', () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
const chunks: string[] = []
const unsub = runtime.subscribeToTerminalData('pty-1', (data) => chunks.push(data))
runtime.onPtyData('pty-1', 'before\n', 100)
unsub()
runtime.onPtyData('pty-1', 'after\n', 101)
expect(chunks).toEqual(['before\n'])
})
it('delivers data exactly once per listener even with multiple subscribers', () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
const chunksA: string[] = []
const chunksB: string[] = []
const unsubA = runtime.subscribeToTerminalData('pty-1', (data) => chunksA.push(data))
runtime.subscribeToTerminalData('pty-1', (data) => chunksB.push(data))
runtime.onPtyData('pty-1', 'hello\n', 100)
unsubA()
runtime.onPtyData('pty-1', 'world\n', 101)
expect(chunksA).toEqual(['hello\n'])
expect(chunksB).toEqual(['hello\n', 'world\n'])
})
})
describe('registerSubscriptionCleanup', () => {
it('cleans up the previous subscription when re-registering the same id', () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
const chunksA: string[] = []
const chunksB: string[] = []
const unsubA = runtime.subscribeToTerminalData('pty-1', (data) => chunksA.push(data))
runtime.registerSubscriptionCleanup('sub-1', () => unsubA())
const unsubB = runtime.subscribeToTerminalData('pty-1', (data) => chunksB.push(data))
runtime.registerSubscriptionCleanup('sub-1', () => unsubB())
runtime.onPtyData('pty-1', 'after-resubscribe\n', 100)
// Why: only the second listener should receive data. The first was
// cleaned up when registerSubscriptionCleanup replaced 'sub-1'.
expect(chunksA).toEqual([])
expect(chunksB).toEqual(['after-resubscribe\n'])
})
it('cleanupSubscription removes the listener', () => {
const runtime = new OrcaRuntimeService(store)
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
const chunks: string[] = []
const unsub = runtime.subscribeToTerminalData('pty-1', (data) => chunks.push(data))
runtime.registerSubscriptionCleanup('sub-1', () => unsub())
runtime.onPtyData('pty-1', 'before\n', 100)
runtime.cleanupSubscription('sub-1')
runtime.onPtyData('pty-1', 'after\n', 101)
expect(chunks).toEqual(['before\n'])
})
})
})