/* 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 } 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: '' }) })) 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], 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('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).toEqual({ 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('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('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() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-hook-test') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-hook-test') 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() }) 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: () => {}, 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('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('removeManagedWorktree PTY teardown (design §4.3)', () => { function createProviderStub( listProcesses: () => Promise<{ id: string; cwd: string; title: string }[]> ): { spawn: ReturnType attach: ReturnType write: ReturnType resize: ReturnType shutdown: ReturnType sendSignal: ReturnType getCwd: ReturnType getInitialCwd: ReturnType clearBuffer: ReturnType acknowledgeDataEvent: ReturnType hasChildProcesses: ReturnType getForegroundProcess: ReturnType serialize: ReturnType revive: ReturnType listProcesses: ReturnType getDefaultShell: ReturnType getProfiles: ReturnType onData: ReturnType onReplay: ReturnType onExit: ReturnType } { return { spawn: vi.fn(), attach: vi.fn(), write: vi.fn(), resize: vi.fn(), shutdown: vi.fn().mockResolvedValue(undefined), sendSignal: vi.fn(), getCwd: vi.fn(), getInitialCwd: vi.fn(), clearBuffer: vi.fn(), acknowledgeDataEvent: vi.fn(), hasChildProcesses: vi.fn(), getForegroundProcess: vi.fn(), serialize: vi.fn(), revive: vi.fn(), listProcesses: vi.fn(listProcesses), getDefaultShell: vi.fn(), getProfiles: vi.fn(), onData: vi.fn().mockReturnValue(() => {}), onReplay: vi.fn().mockReturnValue(() => {}), onExit: vi.fn().mockReturnValue(() => {}) } } it('RPC-initiated delete kills matching PTYs before git', async () => { // Seed the runtime with a live leaf whose worktreeId matches the target. const killSpy = vi.fn().mockReturnValue(true) const localProvider = createProviderStub(async () => []) const callOrder: string[] = [] vi.mocked(removeWorktree).mockImplementation(async () => { callOrder.push('git-removeWorktree') }) const runtime = new OrcaRuntimeService(store, undefined, { getLocalProvider: () => { callOrder.push('getLocalProvider') return localProvider as never } }) runtime.setPtyController({ write: () => true, kill: (id) => { callOrder.push(`kill:${id}`) return killSpy(id) as boolean }, getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') await runtime.removeManagedWorktree(TEST_WORKTREE_ID) expect(killSpy).toHaveBeenCalledWith('pty-1') // The provider-prefix sweep and the git removal must happen AFTER the // runtime-graph kill. Git removal must NOT happen before any kill. const killIdx = callOrder.indexOf('kill:pty-1') const gitIdx = callOrder.indexOf('git-removeWorktree') expect(killIdx).toBeGreaterThanOrEqual(0) expect(gitIdx).toBeGreaterThan(killIdx) }) it('thunk resolves the installed provider lazily, not at construction time', async () => { // Simulates the daemon adapter being installed AFTER OrcaRuntimeService // construction (setLocalPtyProvider(routedAdapter) in daemon-init). // A capture-at-construction refactor would break this test. const preDaemonProvider = createProviderStub(async () => [ { id: '1', cwd: '/tmp', title: 'shell' }, { id: '2', cwd: '/tmp', title: 'shell' } ]) const postDaemonProvider = createProviderStub(async () => [ { id: `${TEST_WORKTREE_ID}@@aaaaaaaa`, cwd: '/tmp', title: 'shell' } ]) let currentProvider: ReturnType = preDaemonProvider const runtime = new OrcaRuntimeService(store, undefined, { getLocalProvider: () => currentProvider as never }) vi.mocked(removeWorktree).mockResolvedValue(undefined) // Simulate daemon-init swapping the provider after construction. currentProvider = postDaemonProvider await runtime.removeManagedWorktree(TEST_WORKTREE_ID) // The post-daemon provider's prefix-matching session must have been // shut down, proving the thunk resolved lazily at call time. expect(postDaemonProvider.shutdown).toHaveBeenCalledWith( `${TEST_WORKTREE_ID}@@aaaaaaaa`, true ) // The pre-daemon provider must not have been consulted for the kill. expect(preDaemonProvider.shutdown).not.toHaveBeenCalled() }) }) })