/* 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 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 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) => ({ ...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) => ({ ...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 = {} 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) => { 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']) }) }) })